<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Aki</title>
    <description>The latest articles on DEV Community by Aki (@datapenguin).</description>
    <link>https://dev.to/datapenguin</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3358661%2Fe003a75e-e7e7-40a0-99ac-f328da87b768.jpg</url>
      <title>DEV Community: Aki</title>
      <link>https://dev.to/datapenguin</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/datapenguin"/>
    <language>en</language>
    <item>
      <title>Connecting Amazon S3 Tables and Snowflake (via the S3 Tables Iceberg REST Endpoint)</title>
      <dc:creator>Aki</dc:creator>
      <pubDate>Fri, 14 Aug 2026 12:26:11 +0000</pubDate>
      <link>https://dev.to/aws-builders/connecting-amazon-s3-tables-and-snowflake-via-the-s3-tables-iceberg-rest-endpoint-a2g</link>
      <guid>https://dev.to/aws-builders/connecting-amazon-s3-tables-and-snowflake-via-the-s3-tables-iceberg-rest-endpoint-a2g</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Original Japanese article&lt;/strong&gt;: &lt;a href="https://zenn.dev/penginpenguin/articles/fcfb6fa2320976" rel="noopener noreferrer"&gt;Amazon S3 TablesとSnowflakeを連携する（S3 Tables Iceberg RESTエンドポイント経由）&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h1&gt;
  
  
  Introduction
&lt;/h1&gt;

&lt;p&gt;I'm Aki, an AWS Community Builder (&lt;a href="https://x.com/jitepengin" rel="noopener noreferrer"&gt;@jitepengin&lt;/a&gt;).&lt;/p&gt;

&lt;p&gt;In an earlier article, I verified how to read from and write to an Iceberg table on S3 Tables using Snowflake's Catalog Integration via the AWS Glue Iceberg REST endpoint.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://dev.to/aws-builders/connecting-amazon-s3-tables-with-snowflake-29n6"&gt;Connecting Amazon S3 Tables with Snowflake&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;That setup routed through Snowflake → the Glue Iceberg REST endpoint → Lake Formation → S3 Tables, and required setting up an IAM policy, a trust policy, registering a Lake Formation data location, and granting permissions across all three levels (Catalog / Database / Table).&lt;/p&gt;

&lt;p&gt;On 2026/8/10, Snowflake's &lt;strong&gt;Amazon S3 Tables Iceberg REST catalog integration&lt;/strong&gt; went GA! This is a new route that connects with SigV4 authentication directly to S3 Tables' native Iceberg REST endpoint, without going through Glue at all.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://docs.snowflake.com/en/release-notes/2026/other/2026-08-10-amazon-s3-tables-iceberg-rest-catalog-integration-ga" rel="noopener noreferrer"&gt;https://docs.snowflake.com/en/release-notes/2026/other/2026-08-10-amazon-s3-tables-iceberg-rest-catalog-integration-ga&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;This time, I wanted to put this new route to the test.&lt;/p&gt;

&lt;h1&gt;
  
  
  Test Environment
&lt;/h1&gt;

&lt;p&gt;I reused the same resources as the previous article.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Table bucket: &lt;code&gt;penguin-rest-test&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Namespace: &lt;code&gt;analytics&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Table: &lt;code&gt;daily_sales&lt;/code&gt; (columns &lt;code&gt;sales_date&lt;/code&gt;, &lt;code&gt;amount&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;Region: &lt;code&gt;ap-northeast-1&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;h1&gt;
  
  
  Architecture
&lt;/h1&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fyczf5zs340bm7sug2ouf.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fyczf5zs340bm7sug2ouf.png" alt=" " width="800" height="343"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;This time's architecture connects the S3 Tables gold layer to Snowflake. The key point is that it uses the Amazon S3 Tables Iceberg REST catalog integration instead of Glue Data Catalog.&lt;/p&gt;

&lt;h1&gt;
  
  
  Differences from the Glue Route (Configuration)
&lt;/h1&gt;

&lt;p&gt;The &lt;code&gt;CATALOG INTEGRATION&lt;/code&gt; parameters differ, as shown below, configured specifically for S3 Tables.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Item&lt;/th&gt;
&lt;th&gt;Via Glue (previous article)&lt;/th&gt;
&lt;th&gt;Direct to S3 Tables (this article)&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;CATALOG_API_TYPE&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;AWS_GLUE&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;AWS_S3TABLES&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;CATALOG_URI&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;glue.&amp;lt;region&amp;gt;.amazonaws.com/iceberg&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;s3tables.&amp;lt;region&amp;gt;.amazonaws.com/iceberg&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;CATALOG_NAME&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;&amp;lt;account_id&amp;gt;:s3tablescatalog/&amp;lt;bucket&amp;gt;&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;arn:aws:s3tables:&amp;lt;region&amp;gt;:&amp;lt;account_id&amp;gt;:bucket/&amp;lt;bucket&amp;gt;&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h1&gt;
  
  
  Differences from the Glue Route (Setup Steps)
&lt;/h1&gt;

&lt;p&gt;Of the steps required in the previous article, the following Lake Formation-related steps simply don't exist here:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Registering the Lake Formation data location (&lt;code&gt;register-resource --with-federation --with-privileged-access&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;Granting Lake Formation permissions across all three levels (Catalog / Database / Table)&lt;/li&gt;
&lt;li&gt;Adding &lt;code&gt;sts:SetSourceIdentity&lt;/code&gt; / &lt;code&gt;sts:SetContext&lt;/code&gt; to the data access role's trust policy&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I wanted to make sure "Lake Formation setup really isn't required" is actually true, so I later confirmed this via CloudTrail.&lt;/p&gt;

&lt;h1&gt;
  
  
  Creating the IAM Role
&lt;/h1&gt;

&lt;p&gt;Starting from the previous article's IAM policy, I removed all Lake Formation- and Glue-related actions, and tested with a minimal-privilege policy scoped down to just &lt;code&gt;s3tables:*&lt;/code&gt; actions.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"Version"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"2012-10-17"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"Statement"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="nl"&gt;"Sid"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"AllowS3TablesDirectAccess"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="nl"&gt;"Effect"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Allow"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="nl"&gt;"Action"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
                &lt;/span&gt;&lt;span class="s2"&gt;"s3tables:GetTableBucket"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
                &lt;/span&gt;&lt;span class="s2"&gt;"s3tables:ListNamespaces"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
                &lt;/span&gt;&lt;span class="s2"&gt;"s3tables:GetNamespace"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
                &lt;/span&gt;&lt;span class="s2"&gt;"s3tables:ListTables"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
                &lt;/span&gt;&lt;span class="s2"&gt;"s3tables:GetTable"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
                &lt;/span&gt;&lt;span class="s2"&gt;"s3tables:GetTableMetadataLocation"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
                &lt;/span&gt;&lt;span class="s2"&gt;"s3tables:UpdateTableMetadataLocation"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
                &lt;/span&gt;&lt;span class="s2"&gt;"s3tables:GetTableData"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
                &lt;/span&gt;&lt;span class="s2"&gt;"s3tables:PutTableData"&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="nl"&gt;"Resource"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
                &lt;/span&gt;&lt;span class="s2"&gt;"arn:aws:s3tables:ap-northeast-1:123456789012:bucket/penguin-rest-test"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
                &lt;/span&gt;&lt;span class="s2"&gt;"arn:aws:s3tables:ap-northeast-1:123456789012:bucket/penguin-rest-test/table/*"&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The key point is that there's no need to include &lt;code&gt;lakeformation:GetDataAccess&lt;/code&gt; or any &lt;code&gt;glue:*&lt;/code&gt; actions.&lt;/p&gt;

&lt;h1&gt;
  
  
  Creating the Catalog Integration
&lt;/h1&gt;

&lt;p&gt;On the Snowflake side, I created a catalog integration that connects directly to the S3 Tables Iceberg REST endpoint.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;OR&lt;/span&gt; &lt;span class="k"&gt;REPLACE&lt;/span&gt; &lt;span class="k"&gt;CATALOG&lt;/span&gt; &lt;span class="n"&gt;INTEGRATION&lt;/span&gt; &lt;span class="n"&gt;penguin_s3tables_direct_int&lt;/span&gt;
  &lt;span class="n"&gt;CATALOG_SOURCE&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;ICEBERG_REST&lt;/span&gt;
  &lt;span class="n"&gt;TABLE_FORMAT&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;ICEBERG&lt;/span&gt;
  &lt;span class="n"&gt;REST_CONFIG&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;CATALOG_URI&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'https://s3tables.ap-northeast-1.amazonaws.com/iceberg'&lt;/span&gt;
    &lt;span class="n"&gt;CATALOG_API_TYPE&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;AWS_S3TABLES&lt;/span&gt;
    &lt;span class="n"&gt;ACCESS_DELEGATION_MODE&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;VENDED_CREDENTIALS&lt;/span&gt;
    &lt;span class="k"&gt;CATALOG_NAME&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'arn:aws:s3tables:ap-northeast-1:123456789012:bucket/penguin-rest-test'&lt;/span&gt;
  &lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="n"&gt;REST_AUTHENTICATION&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="k"&gt;TYPE&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;SIGV4&lt;/span&gt;
    &lt;span class="n"&gt;SIGV4_IAM_ROLE&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'arn:aws:iam::123456789012:role/penguin-s3tables-direct-role'&lt;/span&gt;
    &lt;span class="n"&gt;SIGV4_SIGNING_REGION&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'ap-northeast-1'&lt;/span&gt;
  &lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="n"&gt;ENABLED&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;TRUE&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Compared side by side with the Glue-based article, &lt;code&gt;CATALOG_API_TYPE&lt;/code&gt; changes from &lt;code&gt;AWS_GLUE&lt;/code&gt; to &lt;code&gt;AWS_S3TABLES&lt;/code&gt;, and &lt;code&gt;CATALOG_URI&lt;/code&gt; changes from the Glue Iceberg REST endpoint to S3 Tables' native endpoint. &lt;code&gt;CATALOG_NAME&lt;/code&gt; also now takes the S3 Tables bucket ARN directly, so the previous article's distinctive &lt;code&gt;&amp;lt;account_id&amp;gt;:s3tablescatalog/&amp;lt;bucket&amp;gt;&lt;/code&gt; prefix notation is not used here.&lt;/p&gt;

&lt;h1&gt;
  
  
  Setting Up the Trust Policy
&lt;/h1&gt;

&lt;p&gt;The steps for retrieving Snowflake's IAM user ARN and external ID via &lt;code&gt;DESC CATALOG INTEGRATION&lt;/code&gt;, then setting them on the trust policy of the role specified in &lt;code&gt;SIGV4_IAM_ROLE&lt;/code&gt;, are the same as with the Glue route.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;DESC&lt;/span&gt; &lt;span class="k"&gt;CATALOG&lt;/span&gt; &lt;span class="n"&gt;INTEGRATION&lt;/span&gt; &lt;span class="n"&gt;penguin_s3tables_direct_int&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"Version"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"2012-10-17"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"Statement"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="nl"&gt;"Effect"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Allow"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="nl"&gt;"Principal"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
                &lt;/span&gt;&lt;span class="nl"&gt;"AWS"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"&amp;lt;AWS_IAM_USER_ARN&amp;gt;"&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="nl"&gt;"Action"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"sts:AssumeRole"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="nl"&gt;"Condition"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
                &lt;/span&gt;&lt;span class="nl"&gt;"StringEquals"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
                    &lt;/span&gt;&lt;span class="nl"&gt;"sts:ExternalId"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"&amp;lt;AWS_EXTERNAL_ID&amp;gt;"&lt;/span&gt;&lt;span class="w"&gt;
                &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Everything up to this point is unchanged from the previous article. The difference shows up from here on: we move straight to table creation with no Lake Formation setup at all.&lt;/p&gt;

&lt;h1&gt;
  
  
  Creating the Table
&lt;/h1&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;OR&lt;/span&gt; &lt;span class="k"&gt;REPLACE&lt;/span&gt; &lt;span class="n"&gt;ICEBERG&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;GOLD&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;GOLD&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;s3tables_direct_table&lt;/span&gt;
  &lt;span class="k"&gt;CATALOG&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'penguin_s3tables_direct_int'&lt;/span&gt;
  &lt;span class="n"&gt;CATALOG_NAMESPACE&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'analytics'&lt;/span&gt;
  &lt;span class="n"&gt;CATALOG_TABLE_NAME&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'daily_sales'&lt;/span&gt;
  &lt;span class="n"&gt;AUTO_REFRESH&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;TRUE&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The table was created without any issues, with no Lake Formation data location registration or grants in place.&lt;/p&gt;

&lt;h1&gt;
  
  
  Verifying It Works (Read)
&lt;/h1&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;ALTER&lt;/span&gt; &lt;span class="n"&gt;ICEBERG&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;GOLD&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;GOLD&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;s3tables_direct_table&lt;/span&gt; &lt;span class="n"&gt;REFRESH&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;GOLD&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;GOLD&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;s3tables_direct_table&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fj75xywl8lf4iwsd2x2r3.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fj75xywl8lf4iwsd2x2r3.png" alt=" " width="799" height="596"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Data that had already been loaded via Athena was readable from Snowflake without any problem.&lt;/p&gt;

&lt;h1&gt;
  
  
  Confirming the Lake Formation Calls via CloudTrail
&lt;/h1&gt;

&lt;p&gt;This is the heart of this article. I traced through CloudTrail to see exactly which APIs &lt;code&gt;penguin-s3tables-direct-role&lt;/code&gt; was actually calling.&lt;/p&gt;

&lt;p&gt;Looking at the logs right after running the read operation, this role only calls &lt;code&gt;GetTableBucket&lt;/code&gt;, &lt;code&gt;GetNamespace&lt;/code&gt;, &lt;code&gt;ListNamespaces&lt;/code&gt;, &lt;code&gt;GetTable&lt;/code&gt;, &lt;code&gt;ListTables&lt;/code&gt;, and &lt;code&gt;GetTableMetadataLocation&lt;/code&gt; against &lt;code&gt;s3tables.amazonaws.com&lt;/code&gt;, all of them &lt;code&gt;readOnly: true&lt;/code&gt; events.&lt;/p&gt;

&lt;p&gt;Three things sealed it:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;No &lt;code&gt;invokedBy&lt;/code&gt; field&lt;/strong&gt;&lt;br&gt;
Events from the Glue-based &lt;code&gt;Snowflake_Catalog_Integration&lt;/code&gt; role always carry &lt;code&gt;"invokedBy": "glue.amazonaws.com"&lt;/code&gt;, proof that the resource is being accessed via Glue's service role. Events from the direct role have no such field at all, showing that Snowflake is hitting &lt;code&gt;s3tables.amazonaws.com&lt;/code&gt; directly.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;&lt;code&gt;sourceIPAddress&lt;/code&gt; is a real IP&lt;/strong&gt;&lt;br&gt;
Events from the direct role record Snowflake's actual source IP in &lt;code&gt;sourceIPAddress&lt;/code&gt;, whereas Glue-route events show &lt;code&gt;sourceIPAddress: "glue.amazonaws.com"&lt;/code&gt;, the notation for an internal service-to-service call.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Zero &lt;code&gt;lakeformation.amazonaws.com&lt;/code&gt; events&lt;/strong&gt;&lt;br&gt;
Across the entire test window (roughly 30 minutes, several hundred CloudTrail events including both reads and writes), there were &lt;strong&gt;zero&lt;/strong&gt; &lt;code&gt;lakeformation.amazonaws.com&lt;/code&gt; events attributable to &lt;code&gt;penguin-s3tables-direct-role&lt;/code&gt;. During that same window, &lt;code&gt;Snowflake_Catalog_Integration&lt;/code&gt; (the Glue route) generated a large volume of &lt;code&gt;lakeformation:GetDataAccess&lt;/code&gt; calls, so this isn't a case of events happening to not show up in the log; it's a structural fact that the direct route never goes through Lake Formation.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;h1&gt;
  
  
  Verifying It Works (Write)
&lt;/h1&gt;

&lt;p&gt;Beyond reads, let's confirm writes work too. Since the IAM policy already includes &lt;code&gt;s3tables:PutTableData&lt;/code&gt;, this can be tested with no additional permission changes.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;INSERT&lt;/span&gt; &lt;span class="k"&gt;INTO&lt;/span&gt; &lt;span class="n"&gt;GOLD&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;GOLD&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;s3tables_direct_table&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;sales_date&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;amount&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;VALUES&lt;/span&gt;
    &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'2026-08-14'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;99999&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'2026-08-14'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;88888&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Checking from the Athena side, the data inserted from Snowflake was visible without issue.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="nv"&gt;"analytics"&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nv"&gt;"daily_sales"&lt;/span&gt; &lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;sales_date&lt;/span&gt; &lt;span class="k"&gt;DESC&lt;/span&gt; &lt;span class="k"&gt;LIMIT&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fckiq662pm4535ldstsry.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fckiq662pm4535ldstsry.png" alt=" " width="799" height="258"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;I also checked CloudTrail for the write operation, and just as with the read, found no Lake Formation events attributable to &lt;code&gt;penguin-s3tables-direct-role&lt;/code&gt;.&lt;/p&gt;

&lt;h1&gt;
  
  
  Checking Catalog-Linked Database
&lt;/h1&gt;

&lt;p&gt;In the previous article, I verified automatic table discovery via a Glue-based Catalog-Linked Database (CLD), so I wanted to confirm the same works on the direct route.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;OR&lt;/span&gt; &lt;span class="k"&gt;REPLACE&lt;/span&gt; &lt;span class="k"&gt;DATABASE&lt;/span&gt; &lt;span class="n"&gt;penguin_s3tables_direct_linked_db&lt;/span&gt;
  &lt;span class="n"&gt;LINKED_CATALOG&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="k"&gt;CATALOG&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'penguin_s3tables_direct_int'&lt;/span&gt;
  &lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;As soon as this was created, &lt;code&gt;daily_sales&lt;/code&gt; and &lt;code&gt;daily_sales2&lt;/code&gt; under the &lt;code&gt;analytics&lt;/code&gt; namespace showed up automatically in the table list, with no DDL required.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fvh70cs9b5h1ooo2cpn18.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fvh70cs9b5h1ooo2cpn18.png" alt=" " width="800" height="516"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Confirming Auto-Discovery
&lt;/h2&gt;

&lt;p&gt;I created a new &lt;code&gt;daily_sales3&lt;/code&gt; table from the Athena side, and it was automatically detected on the SnowSight side as well.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="c1"&gt;-- On the Athena side&lt;/span&gt;
&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="nv"&gt;`analytics`&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;daily_sales3&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="n"&gt;sale_date&lt;/span&gt; &lt;span class="nb"&gt;date&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;product_category&lt;/span&gt; &lt;span class="n"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;sales_amount&lt;/span&gt; &lt;span class="nb"&gt;double&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;PARTITIONED&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;month&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;sale_date&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;span class="n"&gt;TBLPROPERTIES&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'table_type'&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'iceberg'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fh92evmb6kd5c3qios5hq.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fh92evmb6kd5c3qios5hq.png" alt=" " width="800" height="537"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Confirming Bidirectional Writes
&lt;/h2&gt;

&lt;p&gt;I inserted into the auto-discovered &lt;code&gt;daily_sales3&lt;/code&gt; from Snowflake, then checked whether it was visible from Athena.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;INSERT&lt;/span&gt; &lt;span class="k"&gt;INTO&lt;/span&gt; &lt;span class="n"&gt;PENGUIN_S3TABLES_DIRECT_LINKED_DB&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;analytics&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;daily_sales3&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;sale_date&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;product_category&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;sales_amount&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;VALUES&lt;/span&gt;
    &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'2026-08-14'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'Electronics'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;15000&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;00&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'2026-08-14'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'Clothing'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;8500&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;50&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'2026-08-14'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'Food'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;3200&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;75&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fi4knlo6v6tco6aub36ap.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fi4knlo6v6tco6aub36ap.png" alt=" " width="799" height="236"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Confirmed without issue on the Athena side. Just like the &lt;code&gt;CREATE ICEBERG TABLE&lt;/code&gt; approach, the CLD approach also works correctly on the direct route.&lt;/p&gt;

&lt;h1&gt;
  
  
  How the Two Routes Compare
&lt;/h1&gt;

&lt;p&gt;Putting the previous article's findings together with this one's:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Item&lt;/th&gt;
&lt;th&gt;Via Glue (previous article)&lt;/th&gt;
&lt;th&gt;Direct to S3 Tables (this article)&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;IAM policy&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;s3tables:*&lt;/code&gt; + &lt;code&gt;lakeformation:GetDataAccess&lt;/code&gt; + &lt;code&gt;glue:*&lt;/code&gt; actions&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;s3tables:*&lt;/code&gt; actions only&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Lake Formation data location registration&lt;/td&gt;
&lt;td&gt;Required (&lt;code&gt;register-resource --with-federation&lt;/code&gt;)&lt;/td&gt;
&lt;td&gt;Not required&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Lake Formation grants (3 levels)&lt;/td&gt;
&lt;td&gt;Required&lt;/td&gt;
&lt;td&gt;Not required&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Trust policy &lt;code&gt;SetSourceIdentity&lt;/code&gt; / &lt;code&gt;SetContext&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Required&lt;/td&gt;
&lt;td&gt;Not required&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;LF calls on read (CloudTrail)&lt;/td&gt;
&lt;td&gt;Occur&lt;/td&gt;
&lt;td&gt;Zero&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;LF calls on write (CloudTrail)&lt;/td&gt;
&lt;td&gt;Occur&lt;/td&gt;
&lt;td&gt;Zero&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;CLD auto-discovery and bidirectional writes&lt;/td&gt;
&lt;td&gt;Succeeded (confirmed in previous article)&lt;/td&gt;
&lt;td&gt;Succeeded (confirmed here)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Read/write operations&lt;/td&gt;
&lt;td&gt;Succeeded (confirmed in previous article)&lt;/td&gt;
&lt;td&gt;Succeeded (confirmed here)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The biggest difference is a clear tradeoff: do you want unified governance through Lake Formation, or a simpler permission design?&lt;/p&gt;

&lt;p&gt;If you want to keep things simple, go with the direct S3 Tables route. If you need unified table management, centralized governance controls, and fine-grained access control, the Glue route is the way to go.&lt;/p&gt;

&lt;h1&gt;
  
  
  Conclusion
&lt;/h1&gt;

&lt;p&gt;This time, I used Snowflake's Amazon S3 Tables Iceberg REST catalog integration, which went GA on 2026/8/10, to verify the new Glue-free direct route.&lt;/p&gt;

&lt;p&gt;To summarize:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The new direct route (&lt;code&gt;CATALOG_API_TYPE = AWS_S3TABLES&lt;/code&gt;) has no Lake Formation-related setup steps in the official documentation.&lt;/li&gt;
&lt;li&gt;Even after completely removing all Lake Formation and Glue actions from the IAM policy, both reads and writes worked without any issues.&lt;/li&gt;
&lt;li&gt;Checking CloudTrail confirmed that &lt;code&gt;lakeformation:GetDataAccess&lt;/code&gt; calls from the direct role were zero throughout the entire test window.&lt;/li&gt;
&lt;li&gt;Events from the direct role carry no &lt;code&gt;invokedBy: glue.amazonaws.com&lt;/code&gt; field, and &lt;code&gt;sourceIPAddress&lt;/code&gt; shows a real IP, further confirming that Snowflake accesses the S3 Tables endpoint directly, without going through Glue.&lt;/li&gt;
&lt;li&gt;Catalog-Linked Database auto-discovery and bidirectional reads/writes also worked correctly on the direct route, and no Lake Formation calls occurred during the CLD's background sync either.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;With the integration verified in this article, using the S3 Tables Iceberg REST endpoint, no Lake Formation-related permissions are needed at all. If you're willing to forgo unified governance through Lake Formation in exchange for a simpler permission design, this looks like a strong option.&lt;/p&gt;

&lt;p&gt;I hope this article is helpful to anyone considering connecting S3 Tables and Snowflake.&lt;/p&gt;

</description>
      <category>aws</category>
      <category>snowflake</category>
      <category>dataengineering</category>
    </item>
    <item>
      <title>A Base Directory Mismatch Between Amazon S3 Tables and Snowflake CLD</title>
      <dc:creator>Aki</dc:creator>
      <pubDate>Tue, 11 Aug 2026 01:18:07 +0000</pubDate>
      <link>https://dev.to/aws-builders/a-base-directory-mismatch-between-amazon-s3-tables-and-snowflake-cld-10md</link>
      <guid>https://dev.to/aws-builders/a-base-directory-mismatch-between-amazon-s3-tables-and-snowflake-cld-10md</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Original Japanese article&lt;/strong&gt;: &lt;a href="https://zenn.dev/penginpenguin/articles/c48e7859b48341" rel="noopener noreferrer"&gt;Amazon S3 Tables×Snowflake CLDでbase directory不整合が発生した話&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h1&gt;
  
  
  Introduction
&lt;/h1&gt;

&lt;p&gt;I'm Aki, an AWS Community Builder (&lt;a href="https://x.com/jitepengin" rel="noopener noreferrer"&gt;@jitepengin&lt;/a&gt;).&lt;/p&gt;

&lt;p&gt;I had a setup where an Iceberg table on S3 Tables was being referenced from Snowflake through a Catalog-Linked Database (CLD). While running it, I hit an interesting error, so I'm sharing the investigation process here.&lt;/p&gt;

&lt;p&gt;While looking at Data Preview for a table in Snowsight, I noticed the following error:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;The parquet file 'data/xxxx/xxxx/xxxx/xxxxxxxx/target_date_month=2026-07/00000-xx-xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx-0-00001.parquet'
for table 'SAMPLE_DB."gold"."fact_daily_sample_metric"' was inaccessible.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Since it was complaining about being unable to access a parquet file, I ran &lt;code&gt;ALTER ICEBERG TABLE ... REFRESH&lt;/code&gt; just to check the state, and got a completely different error this time:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Statement 1 Failed
One of the specified Iceberg metadata files does not conform to the required directory hierarchy.
All files must reside as a strict subpath under the defined base directory.
Current base directory: s3://aaaaaaaa-1111-4aaa-9999-aaaaaaaaaaaa--table-s3/
Conflicting file path: s3://bbbbbbbb-2222-4bbb-8888-bbbbbbbbbbbb--table-s3/metadata/00004-....metadata.json
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This looked nothing like the first error, but once I dug in, both turned out to come from the same root cause.&lt;/p&gt;

&lt;p&gt;Spoiler: the cause wasn't some other table's files leaking in. It was that the same table had been DROPed and re-CREATEd in a short window, and the old warehouse location ended up conflicting with the new one. It looks at first glance like "some other table's files got mixed in," but the mismatch was entirely self-contained within this one table.&lt;/p&gt;

&lt;p&gt;Tracking down the cause took three stages of investigation: verifying metadata via Athena, checking S3 Tables' spec against the official AWS docs, and pinning down the exact events in CloudTrail. This article is a record of that process.&lt;/p&gt;

&lt;h1&gt;
  
  
  Auto-Sync Behavior of the Catalog-Linked Database
&lt;/h1&gt;

&lt;p&gt;This setup uses the Catalog-Linked Database (CLD) I covered in an earlier article.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://dev.to/aws-builders/connecting-amazon-s3-tables-with-snowflake-29n6"&gt;Connecting Amazon S3 Tables with Snowflake&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;CLD links a namespace (database) to a Glue Catalog, in this case backed by S3 Tables. As I confirmed in that earlier article, when a table is created or changed on the AWS side, it's automatically reflected on the Snowflake side without writing any DDL. That's a real operational win (you don't have to run &lt;code&gt;CREATE ICEBERG TABLE&lt;/code&gt; for every single table), but this same "auto-follow" mechanism is also the precondition for the trouble in this article.&lt;/p&gt;

&lt;p&gt;Specifically, CLD doesn't just sync the list of tables and their schemas (columns) under a namespace. It also automatically syncs each table's current metadata pointer (table name → location of the latest &lt;code&gt;metadata.json&lt;/code&gt;). The "base directory" that caused today's problem is part of that synced state on the Snowflake side.&lt;/p&gt;

&lt;p&gt;For what it's worth, the feature itself is genuinely convenient, and I plan to keep using it.&lt;/p&gt;

&lt;h1&gt;
  
  
  Test Environment
&lt;/h1&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fqiazuyht297q2mt183l1.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fqiazuyht297q2mt183l1.png" alt=" " width="800" height="343"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Catalog integration: Snowflake Catalog-Linked Database (&lt;code&gt;CATALOG_SOURCE = ICEBERG_REST&lt;/code&gt;, connecting to S3 Tables via the Glue Iceberg REST endpoint)&lt;/li&gt;
&lt;li&gt;S3 Tables table bucket: &lt;code&gt;sample-s3-table-gold&lt;/code&gt; (anonymized)&lt;/li&gt;
&lt;li&gt;Target table: &lt;code&gt;fact_daily_sample_metric&lt;/code&gt; (anonymized)&lt;/li&gt;
&lt;li&gt;Write job: Glue (Spark), DDL operations via Athena&lt;/li&gt;
&lt;/ul&gt;

&lt;h1&gt;
  
  
  Looking at the Error Message
&lt;/h1&gt;

&lt;p&gt;To recap, the error is essentially saying "you tried to reference a file outside the path registered as the base directory."&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Current base directory: s3://aaaaaaaa-1111-4aaa-9999-aaaaaaaaaaaa--table-s3/
Conflicting file path: s3://bbbbbbbb-2222-4bbb-8888-bbbbbbbbbbbb--table-s3/metadata/00004-....metadata.json
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Both S3 paths point to different UUIDs under a &lt;code&gt;--table-s3&lt;/code&gt; suffix. The first thing I needed to figure out was whether these were genuinely two different tables, or whether this was all happening within a single table.&lt;/p&gt;

&lt;h1&gt;
  
  
  Verifying the Facts via Athena
&lt;/h1&gt;

&lt;h2&gt;
  
  
  Checking the current data file paths with &lt;code&gt;$files&lt;/code&gt;
&lt;/h2&gt;

&lt;p&gt;I used Iceberg's hidden metadata table &lt;code&gt;$files&lt;/code&gt; to check which data file paths the table was currently referencing.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;file_path&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="nv"&gt;"gold"&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nv"&gt;"fact_daily_sample_metric$files"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Every path that came back was under &lt;code&gt;bbbbbbbb-...&lt;/code&gt;. Not a single file referenced &lt;code&gt;aaaaaaaa-...&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Well, that tracks.&lt;/p&gt;

&lt;h2&gt;
  
  
  Going back through history with &lt;code&gt;$snapshots&lt;/code&gt;
&lt;/h2&gt;

&lt;p&gt;Next, I checked the past snapshot history using &lt;code&gt;$snapshots&lt;/code&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;snapshot_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;committed_at&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;manifest_list&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;summary&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="nv"&gt;"gold"&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nv"&gt;"fact_daily_sample_metric$snapshots"&lt;/span&gt;
&lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;committed_at&lt;/span&gt; &lt;span class="k"&gt;ASC&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Even going all the way back to the oldest snapshot, &lt;code&gt;manifest_list&lt;/code&gt; was consistently under &lt;code&gt;bbbbbbbb-...&lt;/code&gt;. So this wasn't a case of "contamination partway through": as far back as I could check, it had consistently referenced &lt;code&gt;bbbbbbbb-...&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Again, that tracks too.&lt;/p&gt;

&lt;p&gt;At this point, all I'd confirmed was that "&lt;code&gt;aaaaaaaa-...&lt;/code&gt; doesn't currently exist as an actual entity in the Glue Catalog." I still had no idea why it was showing up in the error message at all.&lt;/p&gt;

&lt;h1&gt;
  
  
  Checking S3 Tables' Spec in the Official Docs
&lt;/h1&gt;

&lt;p&gt;At this point I decided to step back and check the spec for S3 Tables itself. I referenced the following official docs:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://docs.aws.amazon.com/AmazonS3/latest/userguide/s3-tables-tables.html" rel="noopener noreferrer"&gt;https://docs.aws.amazon.com/AmazonS3/latest/userguide/s3-tables-tables.html&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.aws.amazon.com/AmazonS3/latest/userguide/bucketnamingrules.html" rel="noopener noreferrer"&gt;https://docs.aws.amazon.com/AmazonS3/latest/userguide/bucketnamingrules.html&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;What I confirmed here is that S3 Tables automatically generates a unique warehouse location dedicated to each table, every time a table is created.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;When you create a table, Amazon S3 automatically generates a warehouse location for that table, a unique S3 location that stores the objects associated with it.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;It's also explicitly stated that the &lt;code&gt;--table-s3&lt;/code&gt; suffix is a bucket naming convention reserved exclusively for S3 Tables.&lt;/p&gt;

&lt;p&gt;In other words: if you DROP a table and then CREATE it again under the same name, a brand-new warehouse location with a new UUID gets assigned. This felt like a strong candidate for the identity of the two different UUIDs in the error message.&lt;/p&gt;

&lt;h1&gt;
  
  
  Confirming It With CloudTrail
&lt;/h1&gt;

&lt;p&gt;To test the hypothesis, I searched CloudTrail for this specific table's &lt;code&gt;CreateTable&lt;/code&gt; and &lt;code&gt;DeleteTable&lt;/code&gt; events. Filtering on those two event names, I was able to reconstruct the table's complete history.&lt;/p&gt;

&lt;h2&gt;
  
  
  The table's complete history
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Time&lt;/th&gt;
&lt;th&gt;Event&lt;/th&gt;
&lt;th&gt;Entity UUID&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Day 1, 11:53:37&lt;/td&gt;
&lt;td&gt;Initial &lt;code&gt;CreateTable&lt;/code&gt; (S3 Tables)&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;aaaaaaaa-...&lt;/code&gt; (anonymized)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Day 1, 11:54:13&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;CreateTable&lt;/code&gt; completed (Glue side)&lt;/td&gt;
&lt;td&gt;Same as above&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Day 3, 07:23:47&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;DeleteTable&lt;/code&gt; (Glue + S3 Tables, via Athena)&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;aaaaaaaa-...&lt;/code&gt; deleted&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Day 3, 07:24:08&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;CreateTable&lt;/code&gt; again (Glue + S3 Tables, same actor)&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;bbbbbbbb-...&lt;/code&gt; created&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The gap between the DROP completing (07:23:47) and the re-CREATE completing (07:24:08) was exactly 21 seconds. And these two UUIDs are literally the "base directory" (&lt;code&gt;aaaaaaaa-...&lt;/code&gt;) and "conflicting file path" (&lt;code&gt;bbbbbbbb-...&lt;/code&gt;) from the error message. So this table was created once on Day 1, then DROPed and re-CREATEd on Day 3, and in that process, its physical entity (the warehouse location) was swapped out entirely.&lt;/p&gt;

&lt;p&gt;What this points to is a working hypothesis: even though the table had been recreated, Snowflake's side kept referencing the old location.&lt;/p&gt;

&lt;h1&gt;
  
  
  Why Did CLD Keep Referencing the Old Location?
&lt;/h1&gt;

&lt;p&gt;Everything from here on is speculation, built from what I could confirm plus what's documented officially by Snowflake.&lt;/p&gt;

&lt;p&gt;According to Snowflake's official docs (&lt;a href="https://docs.snowflake.com/en/user-guide/tables-iceberg-catalog-linked-database" rel="noopener noreferrer"&gt;Use a catalog-linked database for Apache Iceberg tables&lt;/a&gt;), CLD polls the remote catalog on a 30-second interval by default (&lt;code&gt;SYNC_INTERVAL_SECONDS&lt;/code&gt;).&lt;/p&gt;

&lt;p&gt;More importantly, CLD actually has two distinct kinds of sync going on:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Table discovery&lt;/strong&gt; (&lt;code&gt;SYNC_INTERVAL_SECONDS&lt;/code&gt;, default 30s): detects tables added to or removed from the namespace&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Table metadata refresh&lt;/strong&gt; (&lt;code&gt;REFRESH_INTERVAL_SECONDS&lt;/code&gt;, default 30s): updates each table's snapshot pointer (the location of the latest &lt;code&gt;metadata.json&lt;/code&gt;)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;There's also a documented mechanism for detecting &lt;code&gt;table-uuid&lt;/code&gt; mismatches during automatic refresh: if the UUID of the external table doesn't match the UUID CLD has on record, the refresh fails and Snowflake drops that table from the CLD (without touching the remote side). So there is, on paper, a mechanism in place to detect that a table's underlying entity has been swapped out.&lt;/p&gt;

&lt;p&gt;In this case, I think a timing conflict like this may have occurred:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Day 3, 07:23:47: the old table (&lt;code&gt;aaaaaaaa-...&lt;/code&gt;) gets DROPed&lt;/li&gt;
&lt;li&gt;21 seconds later, at 07:24:08: a new table with the same name (&lt;code&gt;bbbbbbbb-...&lt;/code&gt;) is re-CREATEd&lt;/li&gt;
&lt;li&gt;In between, CLD's table discovery hadn't yet detected that "the table disappeared" before a same-named new table showed up&lt;/li&gt;
&lt;li&gt;As a result, this got processed not as "table deleted → re-created" but as "existing table updated," leaving the old base directory info in place&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The mechanism for detecting a &lt;code&gt;table-uuid&lt;/code&gt; mismatch and dropping the table from CLD is documented, and I actually confirmed this detection error firing directly, in the reproduction experiment described below. That said, I never observed the table actually getting dropped afterward (more on that below). My best explanation right now is that because the DROP-to-re-CREATE interval (21 seconds) was shorter than both sync intervals (30 seconds), this mechanism didn't get a chance to fire before the change was processed as an "update."&lt;/p&gt;

&lt;p&gt;That said, CLD's internal implementation (exactly when discovery vs. refresh run, and which one wins a race) isn't fully documented, so this remains a hypothesis built from observed facts plus documentation, not something I can call confirmed.&lt;/p&gt;

&lt;h1&gt;
  
  
  Reproduction Experiment
&lt;/h1&gt;

&lt;p&gt;To test the hypothesis, I tried reproducing the same behavior on a small test table.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="c1"&gt;-- 1. Create a test table&lt;/span&gt;
&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;gold&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;test_dropcreate_repro&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="nb"&gt;INT&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;name&lt;/span&gt; &lt;span class="n"&gt;STRING&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;created_at&lt;/span&gt; &lt;span class="nb"&gt;TIMESTAMP&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="c1"&gt;-- 2. Insert one row just to confirm things are working&lt;/span&gt;
&lt;span class="k"&gt;INSERT&lt;/span&gt; &lt;span class="k"&gt;INTO&lt;/span&gt; &lt;span class="n"&gt;gold&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;test_dropcreate_repro&lt;/span&gt;
&lt;span class="k"&gt;VALUES&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'test_row'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;current_timestamp&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="c1"&gt;-- 3. DROP&lt;/span&gt;
&lt;span class="k"&gt;DROP&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;gold&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;test_dropcreate_repro&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;-- 4. Re-CREATE&lt;/span&gt;
&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;gold&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;test_dropcreate_repro&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="nb"&gt;INT&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;name&lt;/span&gt; &lt;span class="n"&gt;STRING&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;created_at&lt;/span&gt; &lt;span class="nb"&gt;TIMESTAMP&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="c1"&gt;-- 5. Insert a row again&lt;/span&gt;
&lt;span class="k"&gt;INSERT&lt;/span&gt; &lt;span class="k"&gt;INTO&lt;/span&gt; &lt;span class="n"&gt;gold&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;test_dropcreate_repro&lt;/span&gt;
&lt;span class="k"&gt;VALUES&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'test_row'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;current_timestamp&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I ran this three times total, and the same mismatch reproduced twice out of three.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Trial&lt;/th&gt;
&lt;th&gt;DROP-to-re-CREATE interval&lt;/th&gt;
&lt;th&gt;Result&lt;/th&gt;
&lt;th&gt;Errors observed&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;(short)&lt;/td&gt;
&lt;td&gt;Not reproduced&lt;/td&gt;
&lt;td&gt;None (data displayed normally)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;(short)&lt;/td&gt;
&lt;td&gt;Reproduced&lt;/td&gt;
&lt;td&gt;"0 Rows" in Data Preview / base directory error on &lt;code&gt;REFRESH&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;(short)&lt;/td&gt;
&lt;td&gt;Reproduced&lt;/td&gt;
&lt;td&gt;Error Code 091391 (UUID mismatch) in Data Preview / base directory error on &lt;code&gt;REFRESH&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  Trial 1: Not reproduced
&lt;/h2&gt;

&lt;p&gt;The first time I tried this sequence, nothing happened. Data Preview showed the data normally in Snowsight, and &lt;code&gt;ALTER ICEBERG TABLE ... REFRESH&lt;/code&gt; didn't throw anything either.&lt;/p&gt;

&lt;h2&gt;
  
  
  Trial 2: Reproduced (data disappears)
&lt;/h2&gt;

&lt;p&gt;Running the same sequence again, with a different DROP-to-CREATE interval, Data Preview showed "0 Rows": the data had disappeared.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fe8s4iv4o78mhh7h5g624.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fe8s4iv4o78mhh7h5g624.png" alt="Trial 2: Data Preview showing 0 Rows" width="800" height="641"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Running &lt;code&gt;ALTER ICEBERG TABLE ... REFRESH&lt;/code&gt; at that point produced the following error:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;One of the specified Iceberg metadata files does not conform to the required directory hierarchy.
All files must reside as a strict subpath under the defined base directory.
Current base directory: s3://cccccccc-3333-4ccc-7777-cccccccccccc--table-s3/.
Conflicting file path: s3://dddddddd-4444-4ddd-6666-dddddddddddd--table-s3/metadata/00001-....metadata.json.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This matched the pattern from the production incident exactly: base directory was still pointing at the old table's warehouse location (&lt;code&gt;cccccccc-...&lt;/code&gt;), while trying to reference the new table's metadata (&lt;code&gt;dddddddd-...&lt;/code&gt;), and failing.&lt;/p&gt;

&lt;h2&gt;
  
  
  Trial 3: Reproduced (UUID mismatch error + base directory error)
&lt;/h2&gt;

&lt;p&gt;Running the same sequence one more time, opening Data Preview in Snowsight this time produced a slightly different error.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fi3fvnc3p2f0s13o5yp1j.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fi3fvnc3p2f0s13o5yp1j.png" alt="Trial 3: UUID mismatch error via Error Code 091391" width="800" height="831"&gt;&lt;/a&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="n"&gt;Error&lt;/span&gt; &lt;span class="n"&gt;Code&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;091391&lt;/span&gt;
&lt;span class="n"&gt;Message&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;SQL&lt;/span&gt; &lt;span class="n"&gt;compilation&lt;/span&gt; &lt;span class="n"&gt;error&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;The&lt;/span&gt; &lt;span class="n"&gt;UUID&lt;/span&gt; &lt;span class="k"&gt;of&lt;/span&gt; &lt;span class="n"&gt;Iceberg&lt;/span&gt; &lt;span class="k"&gt;table&lt;/span&gt; &lt;span class="s1"&gt;'test_dropcreate_repro2'&lt;/span&gt; &lt;span class="n"&gt;does&lt;/span&gt; &lt;span class="k"&gt;not&lt;/span&gt; &lt;span class="k"&gt;match&lt;/span&gt;
&lt;span class="n"&gt;the&lt;/span&gt; &lt;span class="k"&gt;table&lt;/span&gt; &lt;span class="n"&gt;UUID&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="n"&gt;metadata&lt;/span&gt; &lt;span class="n"&gt;file&lt;/span&gt; &lt;span class="s1"&gt;'s3://eeeeeeee-5555-4eee-9999-eeeeeeeeeeee--table-s3/metadata/00001-....metadata.json'&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;On top of that, running &lt;code&gt;ALTER ICEBERG TABLE ... REFRESH&lt;/code&gt; against this same table produced the same "base directory mismatch" error as Trial 2:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;One of the specified Iceberg metadata files does not conform to the required directory hierarchy.
All files must reside as a strict subpath under the defined base directory.
Current base directory: s3://ffffffff-6666-4fff-8888-ffffffffffff--table-s3/.
Conflicting file path: s3://gggggggg-7777-4ggg-5555-gggggggggggg--table-s3/metadata/00001-....metadata.json.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;So in Trial 3, both the "UUID mismatch error (091391)" and the "base directory mismatch error" showed up on the same table. I think this is direct evidence of the &lt;code&gt;table-uuid&lt;/code&gt; mismatch detection mechanism from the docs actually firing. The two errors look different on the surface, but they appear to share the same root cause: the old table's warehouse location still being held as the base directory.&lt;/p&gt;

&lt;p&gt;The docs state that when this mismatch is detected, the refresh fails and Snowflake drops the table from CLD. In my reproduction, though, the table was never actually dropped from CLD after this error occurred. It just stayed there, displayed, with the error persisting. This matches exactly what happened in production too: the table itself continued to exist in CLD the whole time, with the error simply recurring. I wasn't able to determine, within what I observed, exactly when or under what conditions the documented auto-drop behavior actually kicks in.&lt;/p&gt;

&lt;p&gt;This reproduction experiment (3 trials, 2 reproductions) ended up directly supporting the hypothesis laid out above.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The condition described in the hypothesis, "CLD's discovery hadn't detected the DROP before the same-named table showed up," turned out, through the reproduction experiment, to not really be about the absolute length of the interval (whether it's 21 seconds or 30 seconds), but about the relative timing of whether a poll happens to land between the DROP and the CREATE.&lt;/li&gt;
&lt;li&gt;The one out of three trials that didn't reproduce is likely explained by a CLD poll happening to land between the DROP and the CREATE, correctly detecting the deletion. This looks like classic race-condition behavior: the same steps don't necessarily give you the same result every time.&lt;/li&gt;
&lt;li&gt;The &lt;code&gt;table-uuid&lt;/code&gt; mismatch detection mechanism does genuinely exist, and depending on conditions, it can surface as this detection error (Error Code 091391).&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;So "leave a sufficiently long gap" as a mitigation is valid in the sense that a longer gap increases the odds of at least one poll landing in that window, but there's no absolute safe threshold, and it's more accurate to think of it as a way to reduce the probability of hitting this rather than eliminating it.&lt;/p&gt;

&lt;h1&gt;
  
  
  How I Recovered
&lt;/h1&gt;

&lt;p&gt;Since this was a CLD setup, rather than manually recreating the individual table on the Snowflake side, I fixed it from the AWS side (S3 Tables/Glue Catalog). Specifically: DROP the table, wait significantly longer than CLD's sync interval (30 seconds), then re-CREATE it. I never explicitly ran &lt;code&gt;ALTER ICEBERG TABLE&lt;/code&gt; or &lt;code&gt;CREATE OR REPLACE ICEBERG TABLE&lt;/code&gt; from the Snowflake side.&lt;/p&gt;

&lt;p&gt;Since CLD polls the namespace state periodically and reflects it automatically, as long as I left enough of a gap between the DROP and re-CREATE on the AWS side, Snowflake picked up the new table's entity on its own and settled back into a normal state. This doesn't prove that "a short interval was the cause," but it did serve as one supporting data point for the hypothesis (see the reproduction experiment above for more).&lt;/p&gt;

&lt;h1&gt;
  
  
  Things to Keep in Mind Going Forward
&lt;/h1&gt;

&lt;ul&gt;
&lt;li&gt;DROPing and re-CREATEing a table from the catalog side carries a real risk of this kind of mismatch, caused by the combination of S3 Tables regenerating the physical location and CLD's sync timing.&lt;/li&gt;
&lt;li&gt;The reproduction experiment backed up the hypothesis: the real issue isn't the absolute length of the DROP-to-re-CREATE interval, but whether CLD's polling manages to detect the DROP before the re-CREATE completes.&lt;/li&gt;
&lt;li&gt;Because of that, "leave a sufficient gap" helps in the sense that a longer gap raises the odds of a poll landing in between, but it's not a guaranteed-safe threshold, and that's worth keeping in mind.&lt;/li&gt;
&lt;li&gt;Periodically checking &lt;code&gt;$snapshots&lt;/code&gt; and &lt;code&gt;$files&lt;/code&gt; to confirm consistency between a table's actual entity and what's registered in the catalog should make it faster to isolate this kind of issue if it comes up.&lt;/li&gt;
&lt;/ul&gt;

&lt;h1&gt;
  
  
  Conclusion
&lt;/h1&gt;

&lt;p&gt;In this article, I walked through investigating a base directory mismatch I hit while referencing an S3 Tables table through Snowflake CLD, starting with metadata verification in Athena, moving to spec-checking against the official AWS docs, and finishing with pinning down the exact events in CloudTrail.&lt;/p&gt;

&lt;p&gt;To summarize:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The two different UUIDs in the error message weren't from two different tables. They were the "old location" and "new location" of the same table, from a single DROP and re-CREATE.&lt;/li&gt;
&lt;li&gt;I confirmed in the docs that S3 Tables generates a new, unique warehouse location every time a table is (re-)created.&lt;/li&gt;
&lt;li&gt;CloudTrail's actual events confirmed the gap between the DROP completing and the re-CREATE completing was 21 seconds.&lt;/li&gt;
&lt;li&gt;Snowflake CLD has two distinct kinds of sync, table discovery (&lt;code&gt;SYNC_INTERVAL_SECONDS&lt;/code&gt;) and table metadata refresh (&lt;code&gt;REFRESH_INTERVAL_SECONDS&lt;/code&gt;), both defaulting to 30 seconds. There's also a documented protection mechanism that detects &lt;code&gt;table-uuid&lt;/code&gt; mismatches and drops the table from CLD.&lt;/li&gt;
&lt;li&gt;My working theory is that this 21-second DROP-to-re-CREATE interval, being shorter than both default sync intervals (30 seconds), meant table discovery hadn't yet detected the deletion by the time the same-named table reappeared, leaving the old location info in place.&lt;/li&gt;
&lt;li&gt;A reproduction experiment on a small test table (3 trials, 2 reproductions) directly backed up that hypothesis. The real issue isn't the absolute length of the interval, it's the relative timing of whether CLD's polling detects the DROP before the re-CREATE completes. This remains a hypothesis, but reproducing the same error multiple times gives it strong empirical support.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;When you stack an external catalog integration (CLD) on top of an open format like S3 Tables, where a table's physical location can shift dynamically, there's still a black box in how the two catalogs stay in sync. Even a seemingly simple operation like DROP-and-re-CREATE can trigger an unexpected mismatch in a setup like this, which is worth keeping in mind operationally.&lt;/p&gt;

&lt;p&gt;To be clear: this is something I observed in my own environment (Snowflake CLD + S3 Tables), and I'm not claiming it as a confirmed Snowflake bug.&lt;/p&gt;

&lt;p&gt;I hope this is useful to anyone running S3 Tables together with Snowflake CLD.&lt;/p&gt;

</description>
      <category>aws</category>
      <category>snowflake</category>
      <category>dataengineering</category>
    </item>
    <item>
      <title>Comparing AWS Glue Job's Default Mode and VPC Mode</title>
      <dc:creator>Aki</dc:creator>
      <pubDate>Tue, 04 Aug 2026 13:56:41 +0000</pubDate>
      <link>https://dev.to/aws-builders/comparing-aws-glue-jobs-default-mode-and-vpc-mode-4p5c</link>
      <guid>https://dev.to/aws-builders/comparing-aws-glue-jobs-default-mode-and-vpc-mode-4p5c</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Original Japanese article&lt;/strong&gt;: &lt;a href="https://zenn.dev/penginpenguin/articles/01ffa5410d4476" rel="noopener noreferrer"&gt;Glue JobのDefaultモードとVPCモードを比較する&lt;/a&gt; &lt;/p&gt;
&lt;/blockquote&gt;

&lt;h1&gt;
  
  
  Introduction
&lt;/h1&gt;

&lt;p&gt;I'm Aki, an AWS Community Builder (&lt;a href="https://x.com/jitepengin" rel="noopener noreferrer"&gt;@jitepengin&lt;/a&gt;).&lt;/p&gt;

&lt;p&gt;When you use AWS Glue, it's pretty common to run Glue Jobs in default mode, without a VPC at all.&lt;/p&gt;

&lt;p&gt;If your job only needs to reach AWS-managed services like S3 or the Glue Data Catalog, default mode gives you a simpler setup. On the other hand, if you need a more secure environment, or your requirements call for it, you might reach for VPC mode instead.&lt;/p&gt;

&lt;p&gt;I'd always had a vague sense that "VPC mode is probably slower," but I'd never actually measured how much of a difference there is between Default mode and VPC mode.&lt;/p&gt;

&lt;p&gt;In this article, I'll use a simple pipeline — reading from S3 (Raw) and writing into S3 Tables — as a test case, and actually run both connection modes to compare them.&lt;/p&gt;

&lt;h1&gt;
  
  
  Test Environment
&lt;/h1&gt;

&lt;p&gt;I'm reusing the resources from a previous article.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://dev.to/aws-builders/hitting-the-iceberg-rest-catalog-directly-understanding-the-differences-between-glue-data-catalog-43j8"&gt;Hitting the Iceberg REST Catalog Directly: Understanding the Differences Between Glue Data Catalog and S3 Tables&lt;/a&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Table bucket: &lt;code&gt;penguin-rest-test&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Namespace: &lt;code&gt;analytics&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Table: &lt;code&gt;daily_sales&lt;/code&gt; (two columns: &lt;code&gt;sales_date&lt;/code&gt;, &lt;code&gt;amount&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;Raw data location: &lt;code&gt;s3://penguin-raw-test/daily_sales/&lt;/code&gt; (CSV)&lt;/li&gt;
&lt;li&gt;Glue Job: PySpark, G.1X worker, a simple job that reads CSV from S3 and writes it into S3 Tables as Iceberg&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Since this article focuses on network reachability (Default vs. VPC mode), I'm skipping the authorization layer (IAM/Lake Formation) setup here. I'm assuming the table bucket and namespace are already registered, and that the three-tier Catalog/Database/Table grants are already in place, following the steps from a previous article.&lt;/p&gt;

&lt;h1&gt;
  
  
  Test Architecture
&lt;/h1&gt;

&lt;h3&gt;
  
  
  Default Mode
&lt;/h3&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fcgfv32p201m284gooup0.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fcgfv32p201m284gooup0.png" alt="Default mode architecture" width="800" height="453"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;A very simple setup. The job runs on AWS-managed infrastructure without touching your own VPC, so you don't need to think about subnets or security groups.&lt;/p&gt;

&lt;p&gt;Reachable destinations are limited to AWS-provided service endpoints, but for a setup like this one — S3, Glue Data Catalog, and S3 Tables only — that's more than enough.&lt;/p&gt;

&lt;h3&gt;
  
  
  VPC Mode
&lt;/h3&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fbhx53kw5vgrweqi86362.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fbhx53kw5vgrweqi86362.png" alt="VPC mode architecture" width="800" height="433"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The Glue Job creates an ENI inside a private subnet at runtime and reaches each service through a VPC endpoint. A Glue Connection is used to create an ENI (Elastic Network Interface) in the specified subnet, and all traffic goes through it.&lt;/p&gt;

&lt;p&gt;You'd choose this mode when you need reachability to VPC-internal resources like RDS or Redshift, or when you want fine-grained traffic control via security groups and NACLs — in short, when you have stricter security requirements.&lt;/p&gt;

&lt;p&gt;Access to the Glue Data Catalog is officially documented as going "through a local proxy by default." More precisely, this means the traffic is proxied through AWS's Glue-managed VPC to reach the Glue API — which implies there needs to be a network path between that Glue-managed VPC and your own VPC in the first place. In a private subnet setup with no NAT Gateway or Internet Gateway, that path simply doesn't exist, so the local proxy can't do its job and the connection times out. In that case, you need to create an Interface VPC Endpoint for &lt;code&gt;com.amazonaws.&amp;lt;region&amp;gt;.glue&lt;/code&gt;. I actually confirmed this in testing — without a Glue endpoint in place, Iceberg's &lt;code&gt;GlueCatalog&lt;/code&gt; implementation timed out.&lt;/p&gt;

&lt;h3&gt;
  
  
  A Note on ENIs: There Are Two Kinds
&lt;/h3&gt;

&lt;p&gt;One thing worth calling out here is that there are two ENIs with very different characteristics involved:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;The Interface VPC Endpoint's ENI&lt;/strong&gt;: a persistent resource you provision ahead of time. It isn't recreated on every job run — it's billed continuously by the hour as a fixed cost.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The Glue Job's own ENI&lt;/strong&gt;: created inside the subnet on every job run, via the Glue Connection. The network initialization this involves is the extra element unique to VPC mode.&lt;/li&gt;
&lt;/ul&gt;

&lt;h1&gt;
  
  
  Sample Code for Testing
&lt;/h1&gt;

&lt;p&gt;The Glue Job I'm using here is a simple PySpark script that reads a CSV from S3 (Raw) and writes it into an Iceberg table on S3 Tables. The job code itself is identical between Default and VPC mode — the only thing that changes is the network path.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;sys&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;awsglue.transforms&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;awsglue.utils&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;getResolvedOptions&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;pyspark.context&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;SparkContext&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;awsglue.context&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;GlueContext&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;awsglue.job&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;Job&lt;/span&gt;

&lt;span class="n"&gt;args&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;getResolvedOptions&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;sys&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;argv&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;JOB_NAME&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;

&lt;span class="n"&gt;sc&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;SparkContext&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="n"&gt;glueContext&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;GlueContext&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;sc&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;spark&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;glueContext&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;spark_session&lt;/span&gt;
&lt;span class="n"&gt;job&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Job&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;glueContext&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;job&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;init&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;args&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;JOB_NAME&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="n"&gt;args&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="c1"&gt;# Read the CSV from S3 (Raw)
&lt;/span&gt;&lt;span class="n"&gt;raw_df&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;spark&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;read&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;option&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;header&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;true&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;csv&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;s3://penguin-raw-test/daily_sales/&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="n"&gt;raw_df&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;raw_df&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;selectExpr&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;CAST(sales_date AS date) AS sales_date&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;CAST(amount AS long) AS amount&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="c1"&gt;# Write into the S3 Tables Iceberg table (Glue-integrated)
# The catalog is configured ahead of time in Spark to point through s3tablescatalog
&lt;/span&gt;&lt;span class="n"&gt;raw_df&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;writeTo&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;glue_catalog.analytics.daily_sales&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

&lt;span class="n"&gt;job&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;commit&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;On the Spark configuration side (Job Parameters, or &lt;code&gt;%%configure&lt;/code&gt; in a notebook), I'm passing &lt;code&gt;--conf&lt;/code&gt; settings to point the Iceberg catalog at the Glue catalog via &lt;code&gt;s3tablescatalog&lt;/code&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight conf"&gt;&lt;code&gt;--&lt;span class="n"&gt;conf&lt;/span&gt; &lt;span class="n"&gt;spark&lt;/span&gt;.&lt;span class="n"&gt;sql&lt;/span&gt;.&lt;span class="n"&gt;extensions&lt;/span&gt;=&lt;span class="n"&gt;org&lt;/span&gt;.&lt;span class="n"&gt;apache&lt;/span&gt;.&lt;span class="n"&gt;iceberg&lt;/span&gt;.&lt;span class="n"&gt;spark&lt;/span&gt;.&lt;span class="n"&gt;extensions&lt;/span&gt;.&lt;span class="n"&gt;IcebergSparkSessionExtensions&lt;/span&gt;
--&lt;span class="n"&gt;conf&lt;/span&gt; &lt;span class="n"&gt;spark&lt;/span&gt;.&lt;span class="n"&gt;sql&lt;/span&gt;.&lt;span class="n"&gt;catalog&lt;/span&gt;.&lt;span class="n"&gt;glue_catalog&lt;/span&gt;=&lt;span class="n"&gt;org&lt;/span&gt;.&lt;span class="n"&gt;apache&lt;/span&gt;.&lt;span class="n"&gt;iceberg&lt;/span&gt;.&lt;span class="n"&gt;spark&lt;/span&gt;.&lt;span class="n"&gt;SparkCatalog&lt;/span&gt;
--&lt;span class="n"&gt;conf&lt;/span&gt; &lt;span class="n"&gt;spark&lt;/span&gt;.&lt;span class="n"&gt;sql&lt;/span&gt;.&lt;span class="n"&gt;catalog&lt;/span&gt;.&lt;span class="n"&gt;glue_catalog&lt;/span&gt;.&lt;span class="n"&gt;catalog&lt;/span&gt;-&lt;span class="n"&gt;impl&lt;/span&gt;=&lt;span class="n"&gt;org&lt;/span&gt;.&lt;span class="n"&gt;apache&lt;/span&gt;.&lt;span class="n"&gt;iceberg&lt;/span&gt;.&lt;span class="n"&gt;aws&lt;/span&gt;.&lt;span class="n"&gt;glue&lt;/span&gt;.&lt;span class="n"&gt;GlueCatalog&lt;/span&gt;
--&lt;span class="n"&gt;conf&lt;/span&gt; &lt;span class="n"&gt;spark&lt;/span&gt;.&lt;span class="n"&gt;sql&lt;/span&gt;.&lt;span class="n"&gt;catalog&lt;/span&gt;.&lt;span class="n"&gt;glue_catalog&lt;/span&gt;.&lt;span class="n"&gt;io&lt;/span&gt;-&lt;span class="n"&gt;impl&lt;/span&gt;=&lt;span class="n"&gt;org&lt;/span&gt;.&lt;span class="n"&gt;apache&lt;/span&gt;.&lt;span class="n"&gt;iceberg&lt;/span&gt;.&lt;span class="n"&gt;aws&lt;/span&gt;.&lt;span class="n"&gt;s3&lt;/span&gt;.&lt;span class="n"&gt;S3FileIO&lt;/span&gt;
--&lt;span class="n"&gt;conf&lt;/span&gt; &lt;span class="n"&gt;spark&lt;/span&gt;.&lt;span class="n"&gt;sql&lt;/span&gt;.&lt;span class="n"&gt;catalog&lt;/span&gt;.&lt;span class="n"&gt;glue_catalog&lt;/span&gt;.&lt;span class="n"&gt;glue&lt;/span&gt;.&lt;span class="n"&gt;id&lt;/span&gt;=&lt;span class="m"&gt;123456789012&lt;/span&gt;:&lt;span class="n"&gt;s3tablescatalog&lt;/span&gt;/&lt;span class="n"&gt;penguin&lt;/span&gt;-&lt;span class="n"&gt;rest&lt;/span&gt;-&lt;span class="n"&gt;test&lt;/span&gt;
--&lt;span class="n"&gt;conf&lt;/span&gt; &lt;span class="n"&gt;spark&lt;/span&gt;.&lt;span class="n"&gt;sql&lt;/span&gt;.&lt;span class="n"&gt;catalog&lt;/span&gt;.&lt;span class="n"&gt;glue_catalog&lt;/span&gt;.&lt;span class="n"&gt;glue&lt;/span&gt;.&lt;span class="n"&gt;account&lt;/span&gt;-&lt;span class="n"&gt;id&lt;/span&gt;=&lt;span class="m"&gt;123456789012&lt;/span&gt;
--&lt;span class="n"&gt;conf&lt;/span&gt; &lt;span class="n"&gt;spark&lt;/span&gt;.&lt;span class="n"&gt;sql&lt;/span&gt;.&lt;span class="n"&gt;catalog&lt;/span&gt;.&lt;span class="n"&gt;glue_catalog&lt;/span&gt;.&lt;span class="n"&gt;glue&lt;/span&gt;.&lt;span class="n"&gt;region&lt;/span&gt;=&lt;span class="n"&gt;ap&lt;/span&gt;-&lt;span class="n"&gt;northeast&lt;/span&gt;-&lt;span class="m"&gt;1&lt;/span&gt;
--&lt;span class="n"&gt;conf&lt;/span&gt; &lt;span class="n"&gt;spark&lt;/span&gt;.&lt;span class="n"&gt;sql&lt;/span&gt;.&lt;span class="n"&gt;catalog&lt;/span&gt;.&lt;span class="n"&gt;glue_catalog&lt;/span&gt;.&lt;span class="n"&gt;warehouse&lt;/span&gt;=&lt;span class="n"&gt;s3&lt;/span&gt;://&lt;span class="n"&gt;penguin&lt;/span&gt;-&lt;span class="n"&gt;rest&lt;/span&gt;-&lt;span class="n"&gt;test&lt;/span&gt;/
--&lt;span class="n"&gt;conf&lt;/span&gt; &lt;span class="n"&gt;spark&lt;/span&gt;.&lt;span class="n"&gt;sql&lt;/span&gt;.&lt;span class="n"&gt;defaultCatalog&lt;/span&gt;=&lt;span class="n"&gt;glue_catalog&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For a notebook session, you pass this as JSON via the &lt;code&gt;%%configure&lt;/code&gt; magic:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="o"&gt;%%&lt;/span&gt;&lt;span class="n"&gt;configure&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;--conf&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;spark.sql.extensions=org.apache.iceberg.spark.extensions.IcebergSparkSessionExtensions --conf spark.sql.catalog.glue_catalog=org.apache.iceberg.spark.SparkCatalog --conf spark.sql.catalog.glue_catalog.catalog-impl=org.apache.iceberg.aws.glue.GlueCatalog --conf spark.sql.catalog.glue_catalog.io-impl=org.apache.iceberg.aws.s3.S3FileIO --conf spark.sql.catalog.glue_catalog.glue.id=123456789012:s3tablescatalog/penguin-rest-test --conf spark.sql.catalog.glue_catalog.glue.account-id=123456789012 --conf spark.sql.catalog.glue_catalog.glue.region=ap-northeast-1 --conf spark.sql.catalog.glue_catalog.warehouse=s3://penguin-rest-test/ --conf spark.sql.defaultCatalog=glue_catalog&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;--datalake-formats&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;iceberg&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;To run in VPC mode, you first need to create a Glue Connection. The &lt;code&gt;ConnectionType&lt;/code&gt; is set to &lt;code&gt;NETWORK&lt;/code&gt;. Unlike connection types for data stores such as JDBC, this one simply grants reachability to a subnet inside your VPC.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;aws glue create-connection &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--connection-input&lt;/span&gt; &lt;span class="s1"&gt;'{
    "Name": "penguin-vpc-connection",
    "ConnectionType": "NETWORK",
    "PhysicalConnectionRequirements": {
      "SubnetId": "subnet-xxxxxxxxxxxxxxxxx",
      "SecurityGroupIdList": ["sg-xxxxxxxxxxxxxxxxx"],
      "AvailabilityZone": "ap-northeast-1a"
    }
  }'&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--region&lt;/span&gt; ap-northeast-1
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;On the Glue Job side, you just add this connection name under &lt;code&gt;Connections&lt;/code&gt; to switch it into VPC mode. For the Default mode job, you leave &lt;code&gt;Connections&lt;/code&gt; unset and use it as-is.&lt;/p&gt;

&lt;p&gt;For measuring execution time, I use &lt;code&gt;ExecutionTime&lt;/code&gt;, &lt;code&gt;StartedOn&lt;/code&gt;, and &lt;code&gt;CompletedOn&lt;/code&gt; from &lt;code&gt;get_job_run&lt;/code&gt;. &lt;code&gt;StartedOn&lt;/code&gt; is defined in the official API reference only as "the time at which this job run was started" — it doesn't specify whether that's the transition into the &lt;code&gt;STARTING&lt;/code&gt; state or the &lt;code&gt;RUNNING&lt;/code&gt; state.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;ExecutionTime&lt;/code&gt;, on the other hand, is officially defined as "the amount of time the job run consumed resources," which means it can include internal Glue processing time — such as &lt;code&gt;SparkContext&lt;/code&gt; initialization — in addition to the actual ETL processing done by user code.&lt;/p&gt;

&lt;p&gt;If you want to isolate pure ETL processing time, you'll need to separately check the CloudWatch metric &lt;code&gt;glue.driver.aggregate.elapsedTime&lt;/code&gt; (ETL elapsed time that excludes bootstrap time). For even finer-grained measurement, tracking &lt;code&gt;JobRunState&lt;/code&gt; transitions (&lt;code&gt;STARTING&lt;/code&gt;, &lt;code&gt;RUNNING&lt;/code&gt;, &lt;code&gt;SUCCEEDED&lt;/code&gt;) via EventBridge would be a more precise approach.&lt;/p&gt;

&lt;h1&gt;
  
  
  Execution Time Comparison Results
&lt;/h1&gt;

&lt;p&gt;For both Default and VPC mode, I pulled the full job run history via &lt;code&gt;get-job-runs&lt;/code&gt; and compared total execution time (&lt;code&gt;StartedOn&lt;/code&gt; to &lt;code&gt;CompletedOn&lt;/code&gt;), &lt;code&gt;ExecutionTime&lt;/code&gt;, and overhead (total time minus &lt;code&gt;ExecutionTime&lt;/code&gt;). After excluding values that deviated significantly from the rest of the same-mode results (discussed below), I ended up with 15 samples for each mode.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Mode&lt;/th&gt;
&lt;th&gt;Total time (mean / min / max / stddev)&lt;/th&gt;
&lt;th&gt;ExecutionTime (mean / min / max / stddev)&lt;/th&gt;
&lt;th&gt;Overhead (mean / min / max / stddev)&lt;/th&gt;
&lt;th&gt;n&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Default&lt;/td&gt;
&lt;td&gt;80.2s / 62.5s / 110.3s / 11.6s&lt;/td&gt;
&lt;td&gt;73.1s / 58s / 98s / 9.7s&lt;/td&gt;
&lt;td&gt;7.1s / 4.5s / 12.3s / 2.4s&lt;/td&gt;
&lt;td&gt;15&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;VPC&lt;/td&gt;
&lt;td&gt;97.9s / 69.0s / 133.5s / 19.0s&lt;/td&gt;
&lt;td&gt;87.7s / 64s / 109s / 14.1s&lt;/td&gt;
&lt;td&gt;10.1s / 5.0s / 24.5s / 6.6s&lt;/td&gt;
&lt;td&gt;15&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The mean differences were +17.7s for total time, +14.7s for &lt;code&gt;ExecutionTime&lt;/code&gt;, and +3.0s for overhead — VPC mode came out larger on every metric. Even accounting for the standard deviations (11.6s for Default, 19.0s for VPC on total time), VPC mode was consistently higher across the sample range I collected.&lt;/p&gt;

&lt;p&gt;The gap in standard deviation itself is also worth noting: VPC mode's stddev is about 1.6x Default's (19.0s vs. 11.6s). In other words, VPC mode isn't just slower on average — its execution time is also more variable. This suggests that ENI creation and Interface VPC Endpoint API calls may be introducing latency that fluctuates from run to run.&lt;/p&gt;

&lt;p&gt;The excluded outliers were two Default runs (150s and 146s &lt;code&gt;ExecutionTime&lt;/code&gt;) and one VPC run (177s). All three deviated clearly from the rest of the samples within their own mode, so I excluded them from the statistics. Concretely, Default's normal values topped out at 98s, while the outliers were 146–150s (roughly 1.5–1.6x); VPC's normal values topped out at 109s, while its outlier was 177s (roughly 1.6x). Both sets of outliers are clearly separated from the rest of their mode's values.&lt;/p&gt;

&lt;p&gt;Going in, I'd expected &lt;code&gt;ExecutionTime&lt;/code&gt; to be roughly the same between the two modes, with only the ENI-creation overhead being larger for VPC mode. What I actually found was that the overhead gap (+3.0s) supports the &lt;em&gt;direction&lt;/em&gt; of that hypothesis, but doesn't fully explain the result on its own. The absolute gap is larger on the &lt;code&gt;ExecutionTime&lt;/code&gt; side (+14.7s) — so "the difference is just overhead" doesn't hold up.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why the Gap Isn't Fully Explained by Overhead Alone
&lt;/h2&gt;

&lt;p&gt;As for why ENI creation isn't showing up as as large an overhead as I'd expected, I have a few hypotheses:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Hypothesis 1&lt;/strong&gt;: &lt;code&gt;ExecutionTime&lt;/code&gt; is defined as "the time the job run spent consuming resources." It's possible that ENI creation is counted within the &lt;code&gt;ExecutionTime&lt;/code&gt; window itself, rather than in the &lt;code&gt;StartedOn&lt;/code&gt;-to-&lt;code&gt;ExecutionTime&lt;/code&gt;-start gap I'm calling "overhead." Given that the overhead gap is only +3.0s while the total-time gap is +17.7s, it would make sense if most of the ENI-creation cost is being attributed to &lt;code&gt;ExecutionTime&lt;/code&gt; rather than overhead.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Hypothesis 2&lt;/strong&gt;: Now that access to the Glue Data Catalog goes through an Interface VPC Endpoint instead of the local proxy, the catalog API calls themselves might be taking longer. That said, Interface VPC Endpoint API latency is generally on the order of single-digit to double-digit milliseconds, which feels too small to account for a 14.7-second gap on its own. It's conceivable that repeated catalog API calls add up, but I don't think this is strong enough evidence to call it the primary cause.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Hypothesis 3&lt;/strong&gt;: A combination of factors specific to VPC mode — network initialization for the Glue execution environment, DNS resolution, and Interface VPC Endpoint-based service access — could be contributing together.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;None of these has solid supporting evidence; the only things I can say for certain are the facts drawn from the measurements themselves. Across 15 samples per mode, VPC mode ran about 17.7 seconds longer on average in total, with roughly 14.7 seconds of that on the &lt;code&gt;ExecutionTime&lt;/code&gt; side and about 3.0 seconds on the overhead side. Every hypothesis here is speculation drawn from that data — including whether ENI creation is even included in the &lt;code&gt;ExecutionTime&lt;/code&gt; measurement window — and none of it is backed by official documentation. Measuring more granularly within the &lt;code&gt;ExecutionTime&lt;/code&gt; window could help confirm or rule these out.&lt;/p&gt;

&lt;h1&gt;
  
  
  Choosing Between Default and VPC Mode
&lt;/h1&gt;

&lt;h2&gt;
  
  
  Cost Comparison
&lt;/h2&gt;

&lt;p&gt;What VPC mode adds is the hourly cost of Interface VPC Endpoints (for Glue and for S3 Tables). Access to CloudWatch Logs continues to go through the local proxy even with &lt;code&gt;--disable-proxy-v2&lt;/code&gt; enabled, so no additional Interface VPC Endpoint is needed there. Access to the Glue Data Catalog, however, does need one — in a private subnet setup without a NAT Gateway or Internet Gateway, the local proxy has no path to the Glue-managed VPC and simply doesn't work (more on this below).&lt;/p&gt;

&lt;p&gt;I checked the actual rate in the AWS Pricing Calculator for the Tokyo region (Amazon VPC → AWS PrivateLink), and an Interface VPC Endpoint comes out to $0.014 per endpoint, per AZ, per hour.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Glue endpoint (Interface):        1 AZ × $0.014/hour
S3 Tables endpoint (Interface):   1 AZ × $0.014/hour

2 endpoints × 1 AZ × $0.014/hour × 730 hours/month
= ~$20.44/month (single-AZ setup)

For a multi-AZ (2 AZ) setup:
2 endpoints × 2 AZ × $0.014/hour × 730 hours/month
= ~$40.88/month
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;On top of that, there's a data processing charge based on the volume passing through each endpoint (for the first PB per month, $0.01/GB). For a small data movement like this S3 Raw → S3 Tables job, that charge is negligible — most of the monthly cost comes from the hourly endpoint charge.&lt;/p&gt;

&lt;p&gt;Default mode doesn't carry any of these fixed costs, so for a setup where everything is reachable via AWS public services, you're looking at roughly a $20–40/month cost difference (single-AZ to multi-AZ).&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Item&lt;/th&gt;
&lt;th&gt;Default mode&lt;/th&gt;
&lt;th&gt;VPC mode&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Interface VPC Endpoint (Glue)&lt;/td&gt;
&lt;td&gt;Not needed&lt;/td&gt;
&lt;td&gt;Needed (per AZ)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Interface VPC Endpoint (S3 Tables)&lt;/td&gt;
&lt;td&gt;Not needed&lt;/td&gt;
&lt;td&gt;Needed (per AZ)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Gateway VPC Endpoint (S3)&lt;/td&gt;
&lt;td&gt;Not needed&lt;/td&gt;
&lt;td&gt;Needed (free)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;CloudWatch Logs traffic&lt;/td&gt;
&lt;td&gt;Via AWS-managed network&lt;/td&gt;
&lt;td&gt;Via local proxy (no endpoint needed)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Job startup overhead&lt;/td&gt;
&lt;td&gt;None&lt;/td&gt;
&lt;td&gt;ENI creation (on every run)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;One thing worth noting: S3 Tables routes different kinds of operations through different endpoints. Table bucket, namespace, and table creation/deletion operations go through the dedicated S3 Tables endpoint (&lt;code&gt;s3tables.&amp;lt;region&amp;gt;.amazonaws.com&lt;/code&gt;), while object-level operations — reading and writing data files and metadata files — go through the S3 endpoint (&lt;code&gt;s3.&amp;lt;region&amp;gt;.amazonaws.com&lt;/code&gt;). This &lt;code&gt;s3tables&lt;/code&gt; endpoint can't be substituted with the S3 Gateway Endpoint; it needs to be created separately as an Interface endpoint. AWS's own documentation recommends creating two separate VPC endpoints — one for S3, one for S3 Tables — when accessing S3 Tables, which is exactly what this setup does.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;aws ec2 create-vpc-endpoint &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--vpc-id&lt;/span&gt; vpc-id &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--service-name&lt;/span&gt; com.amazonaws.ap-northeast-1.s3tables &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--subnet-ids&lt;/span&gt; subnet-1 subnet-2 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--vpc-endpoint-type&lt;/span&gt; Interface &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--ip-address-type&lt;/span&gt; dualstack &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--dns-options&lt;/span&gt; &lt;span class="s2"&gt;"DnsRecordIpType=dualstack"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--security-group-ids&lt;/span&gt; sg-id &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--region&lt;/span&gt; ap-northeast-1
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For CloudWatch Logs, the documentation states that even with &lt;code&gt;--disable-proxy-v2&lt;/code&gt; enabled, traffic continues going through the local proxy — so no additional Interface VPC Endpoint is needed there.&lt;/p&gt;

&lt;p&gt;Access to the Glue Data Catalog is a different story, though. The local proxy mechanism relies on proxying requests through the Glue-managed VPC to reach the Glue API — and in a private subnet setup without a NAT Gateway or Internet Gateway, there's no path between your VPC and that Glue-managed VPC, so the proxy simply doesn't function. I ran into this directly during testing: connections to &lt;code&gt;glue.&amp;lt;region&amp;gt;.amazonaws.com&lt;/code&gt; timed out, and creating an Interface VPC Endpoint for Glue (&lt;code&gt;com.amazonaws.&amp;lt;region&amp;gt;.glue&lt;/code&gt;) resolved it.&lt;/p&gt;

&lt;p&gt;As for STS, in a basic setup like this one — without Lake Formation credential vending — I found no evidence that it's needed. So for a setup like this, the conclusion is that you need two Interface VPC Endpoints: one for Glue, one for S3 Tables.&lt;/p&gt;

&lt;h2&gt;
  
  
  Thinking in Terms of Responsibility
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Default mode&lt;/strong&gt;: hands off responsibility for network control to AWS. You can focus purely on IAM permissions and job logic.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;VPC mode&lt;/strong&gt;: you own network control yourself, all the way down to subnet design, security groups, and endpoint placement.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This mirrors a structure I laid out in an earlier article about where authorization responsibility sits across IAM, Lake Formation, and s3tables IAM actions. That same lens — where does the responsibility live — applies just as well to Glue's network configuration.&lt;/p&gt;

&lt;h2&gt;
  
  
  Thinking in Terms of Security Requirements
&lt;/h2&gt;

&lt;p&gt;VPC mode becomes necessary mainly in two situations:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;You need reachability to VPC-internal resources like RDS, Redshift, or on-prem systems&lt;/li&gt;
&lt;li&gt;You have an explicit requirement to keep your traffic fully private end-to-end (audit or compliance requirements, for example)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Conversely, if everything you're reaching is an AWS public service — S3, Glue Data Catalog, S3 Tables — there's not much of a reason to actively reach for VPC mode.&lt;/p&gt;

&lt;p&gt;For a setup like this one, where everything is contained within S3, Glue Data Catalog, and S3 Tables, I'd treat Default mode as the first thing to reach for.&lt;/p&gt;

&lt;p&gt;That said, this isn't really a "VPC is safer" vs. "Default is simpler" kind of choice. What you're actually deciding is how much of the responsibility for network control you want to hold onto yourself, within whatever scope is genuinely necessary. If you have a clear reason to own that responsibility — reachability into VPC-internal resources, an audit requirement — go with VPC mode. Otherwise, it's reasonable to hand it off to AWS.&lt;/p&gt;

&lt;h1&gt;
  
  
  Other Notes
&lt;/h1&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Security groups need a self-referencing rule&lt;/strong&gt;: to allow communication between the Glue Spark driver and executors, the security group you specify needs an inbound rule that allows all TCP traffic from itself as the source. Without this, the job can start but internal communication will fail.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Overly restrictive outbound rules can break ENI creation&lt;/strong&gt;: the security group needs to allow HTTPS to the Glue and S3 Tables Interface VPC Endpoints, so make sure port 443/TCP is open to those endpoints. It's safer to start permissive and tighten gradually.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A Glue endpoint is required in a private subnet without NAT&lt;/strong&gt;: without a NAT Gateway or Internet Gateway, the local proxy for Glue Data Catalog access won't work, and connections to &lt;code&gt;glue.&amp;lt;region&amp;gt;.amazonaws.com&lt;/code&gt; will time out. Creating an Interface VPC Endpoint for &lt;code&gt;com.amazonaws.&amp;lt;region&amp;gt;.glue&lt;/code&gt; resolves this.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;VPC DNS settings&lt;/strong&gt;: to run in VPC mode, both &lt;code&gt;enableDnsHostnames&lt;/code&gt; and &lt;code&gt;enableDnsSupport&lt;/code&gt; need to be &lt;code&gt;true&lt;/code&gt; on the VPC. If they're not, Glue will fail to resolve hostnames — worth checking ahead of time.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Verifying the Gateway Endpoint path&lt;/strong&gt;: VPC Flow Logs alone aren't enough to confidently confirm that traffic is going through the Gateway Endpoint. Since Gateway endpoints resolve via prefix lists in the route table, checking the route table configuration directly is more reliable. In this environment's route table, I found an active route with a destination of &lt;code&gt;pl-xxxxxxxx&lt;/code&gt; (the S3 prefix list) and a target pointing to the Gateway VPC Endpoint I'd created — confirming that S3-bound traffic does resolve through that endpoint.&lt;/li&gt;
&lt;/ul&gt;

&lt;h1&gt;
  
  
  Conclusion
&lt;/h1&gt;

&lt;p&gt;In this article, I used an S3 (Raw) → S3 Tables pipeline as a test case to compare AWS Glue Job's Default mode and VPC mode with actual measurements.&lt;/p&gt;

&lt;p&gt;To summarize:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Default mode reaches AWS public services over AWS-managed networking, without touching your own VPC.&lt;/li&gt;
&lt;li&gt;VPC mode creates an ENI in a subnet via a Glue Connection and routes traffic through it. It's the right choice when you need reachability to VPC-internal resources or full control over your network path.&lt;/li&gt;
&lt;li&gt;Under the conditions tested here (G.1X, a simple S3 → S3 Tables write, 15 samples per mode), VPC mode ran about 17.7 seconds longer on average in total execution time. Of that, roughly 14.7 seconds showed up on the &lt;code&gt;ExecutionTime&lt;/code&gt; side and about 3.0 seconds on the overhead side — meaning the gap was larger on the &lt;code&gt;ExecutionTime&lt;/code&gt; side than my original hypothesis (that the difference would come entirely from ENI-creation overhead) predicted, and overhead alone doesn't explain it.&lt;/li&gt;
&lt;li&gt;VPC mode's cost has two distinct components: the ongoing hourly charge for Interface VPC Endpoints (fixed cost), and network-initialization latency incurred on every job run.&lt;/li&gt;
&lt;li&gt;In terms of responsibility, Default mode is the simpler choice that hands network control to AWS, while VPC mode means taking on that control yourself, in exchange for meeting security requirements or reaching resources inside your VPC.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Network architecture decisions tend to stick around once you make them — there's rarely a natural point to revisit them later. Having actual numbers on hand for how much of a difference a choice like this makes should be useful the next time a similar decision comes up.&lt;/p&gt;

&lt;p&gt;I hope this article is useful to anyone weighing network configuration options for their Glue Jobs.&lt;/p&gt;

</description>
      <category>aws</category>
      <category>dataengineering</category>
    </item>
    <item>
      <title>Comparing the Permission Models of the Iceberg REST APIs Provided by Glue Data Catalog and S3 Tables</title>
      <dc:creator>Aki</dc:creator>
      <pubDate>Tue, 28 Jul 2026 00:07:18 +0000</pubDate>
      <link>https://dev.to/aws-builders/comparing-the-permission-models-of-the-iceberg-rest-apis-provided-by-glue-data-catalog-and-s3-tables-4578</link>
      <guid>https://dev.to/aws-builders/comparing-the-permission-models-of-the-iceberg-rest-apis-provided-by-glue-data-catalog-and-s3-tables-4578</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Original Japanese article&lt;/strong&gt;: &lt;a href="https://zenn.dev/penginpenguin/articles/83a0c1f0e57f5f" rel="noopener noreferrer"&gt;Glue Data CatalogとS3TablesのそれぞれのIceberg REST APIの権限モデルを比べてみる&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h1&gt;
  
  
  Introduction
&lt;/h1&gt;

&lt;p&gt;I'm Aki, an AWS Community Builder (&lt;a href="https://x.com/jitepengin" rel="noopener noreferrer"&gt;@jitepengin&lt;/a&gt;).&lt;/p&gt;

&lt;p&gt;Across two previous articles, I hit the Iceberg REST Catalog directly and compared the design of the Glue endpoint and the S3 Tables endpoint:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://dev.to/aws-builders/hitting-the-iceberg-rest-catalog-directly-understanding-the-differences-between-glue-data-catalog-43j8"&gt;Hitting the Iceberg REST Catalog Directly: Understanding the Differences Between Glue Data Catalog and S3 Tables&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dev.to/aws-builders/verifying-how-iam-and-lake-formation-behave-for-the-glue-rest-catalog-and-s3-tables-2cgc"&gt;Verifying How IAM and Lake Formation Behave for the Glue REST Catalog and S3 Tables&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Both of those took the angle of "looking at the same table through two different entry points." This time I wanted to try a different angle: laying the official per-operation permission mapping tables for both endpoints side by side.&lt;/p&gt;

&lt;p&gt;For both endpoints, AWS publishes a table mapping each Iceberg REST operation to its IAM action, its Lake Formation permission, and its CloudTrail event name. Just placing these two tables next to each other turned up some interesting findings and design differences, which I've summarized below.&lt;/p&gt;

&lt;p&gt;This time, rather than hands-on testing like before, this is a documentation-based comparison. Wherever something lines up with what I actually verified in the previous articles, I've noted that explicitly.&lt;/p&gt;

&lt;p&gt;The official documentation I referenced:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Glue side: &lt;a href="https://docs.aws.amazon.com/glue/latest/dg/iceberg-rest-apis.html" rel="noopener noreferrer"&gt;https://docs.aws.amazon.com/glue/latest/dg/iceberg-rest-apis.html&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;S3 Tables side: &lt;a href="https://docs.aws.amazon.com/AmazonS3/latest/userguide/s3-tables-integrating-open-source.html" rel="noopener noreferrer"&gt;https://docs.aws.amazon.com/AmazonS3/latest/userguide/s3-tables-integrating-open-source.html&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;h1&gt;
  
  
  Lining Up the Permission Mapping by Operation
&lt;/h1&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Operation&lt;/th&gt;
&lt;th&gt;Glue: IAM action&lt;/th&gt;
&lt;th&gt;Glue: LF permission&lt;/th&gt;
&lt;th&gt;S3 Tables: IAM action&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;getConfig&lt;/td&gt;
&lt;td&gt;&lt;code&gt;glue:GetCatalog&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Not required&lt;/td&gt;
&lt;td&gt;&lt;code&gt;s3tables:GetTableBucket&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;listNamespaces&lt;/td&gt;
&lt;td&gt;&lt;code&gt;glue:GetDatabase&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;ALL, DESCRIBE, SELECT&lt;/td&gt;
&lt;td&gt;&lt;code&gt;s3tables:ListNamespaces&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;createNamespace&lt;/td&gt;
&lt;td&gt;&lt;code&gt;glue:CreateDatabase&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;ALL, DESCRIBE, SELECT&lt;/td&gt;
&lt;td&gt;&lt;code&gt;s3tables:CreateNamespace&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;loadNamespaceMetadata&lt;/td&gt;
&lt;td&gt;&lt;code&gt;glue:GetDatabase&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;ALL, DESCRIBE, SELECT&lt;/td&gt;
&lt;td&gt;&lt;code&gt;s3tables:GetNamespace&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;updateNamespaceProperties&lt;/td&gt;
&lt;td&gt;&lt;code&gt;glue:UpdateDatabase&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;ALL, ALTER&lt;/td&gt;
&lt;td&gt;(not listed)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;dropNamespace&lt;/td&gt;
&lt;td&gt;&lt;code&gt;glue:DeleteDatabase&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;ALL, DROP&lt;/td&gt;
&lt;td&gt;&lt;code&gt;s3tables:DeleteNamespace&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;namespaceExists&lt;/td&gt;
&lt;td&gt;(not listed)&lt;/td&gt;
&lt;td&gt;-&lt;/td&gt;
&lt;td&gt;&lt;code&gt;s3tables:GetNamespace&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;listTables&lt;/td&gt;
&lt;td&gt;&lt;code&gt;glue:GetTables&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;ALL, SELECT, DESCRIBE&lt;/td&gt;
&lt;td&gt;&lt;code&gt;s3tables:ListTables&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;createTable&lt;/td&gt;
&lt;td&gt;&lt;code&gt;glue:CreateTable&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;ALL, CREATE_TABLE&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;s3tables:CreateTable&lt;/code&gt; + &lt;code&gt;s3tables:PutTableData&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;loadTable&lt;/td&gt;
&lt;td&gt;&lt;code&gt;glue:GetTable&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;ALL, SELECT, DESCRIBE&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;s3tables:GetTableMetadataLocation&lt;/code&gt; + &lt;code&gt;s3tables:GetTableData&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;updateTable&lt;/td&gt;
&lt;td&gt;&lt;code&gt;glue:UpdateTable&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;ALL, ALTER&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;s3tables:UpdateTableMetadataLocation&lt;/code&gt; + &lt;code&gt;s3tables:PutTableData&lt;/code&gt; + &lt;code&gt;s3tables:GetTableData&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;dropTable&lt;/td&gt;
&lt;td&gt;&lt;code&gt;glue:DeleteTable&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;ALL, DROP&lt;/td&gt;
&lt;td&gt;&lt;code&gt;s3tables:DeleteTable&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;renameTable&lt;/td&gt;
&lt;td&gt;(not listed; only possible via the extension API's UpdateTable)&lt;/td&gt;
&lt;td&gt;-&lt;/td&gt;
&lt;td&gt;&lt;code&gt;s3tables:RenameTable&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;tableExists&lt;/td&gt;
&lt;td&gt;&lt;code&gt;glue:GetTable&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;ALL, SELECT, DESCRIBE&lt;/td&gt;
&lt;td&gt;&lt;code&gt;s3tables:GetTable&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Even just laying these side by side surfaces several differences.&lt;/p&gt;

&lt;h1&gt;
  
  
  The Number of IAM Actions Differs for Data Operations
&lt;/h1&gt;

&lt;p&gt;The first thing that jumps out is here. For operations that touch data, such as &lt;code&gt;loadTable&lt;/code&gt;, &lt;code&gt;createTable&lt;/code&gt;, and &lt;code&gt;updateTable&lt;/code&gt;, the number of IAM actions required differs between the two.&lt;/p&gt;

&lt;p&gt;The Glue endpoint's &lt;code&gt;loadTable&lt;/code&gt; needs only one action, &lt;code&gt;glue:GetTable&lt;/code&gt;, while the S3 Tables endpoint's &lt;code&gt;loadTable&lt;/code&gt; needs two: &lt;code&gt;s3tables:GetTableMetadataLocation&lt;/code&gt; and &lt;code&gt;s3tables:GetTableData&lt;/code&gt;. &lt;code&gt;createTable&lt;/code&gt; and &lt;code&gt;updateTable&lt;/code&gt; show the same pattern: the S3 Tables side splits requirements into a "metadata operation action" and a "data body operation action."&lt;/p&gt;

&lt;p&gt;This lines up with something I confirmed in a previous article: calling S3 Tables' &lt;code&gt;LoadTable&lt;/code&gt; logs a &lt;code&gt;GetTableMetadataLocation&lt;/code&gt; management event in CloudTrail.&lt;/p&gt;

&lt;p&gt;What this suggests is that S3 Tables cleanly separates access to metadata from access to the data body at the API level, while the Glue endpoint consolidates everything into the single &lt;code&gt;glue:GetTable&lt;/code&gt; action and delegates the actual storage access to Lake Formation's vended credentials. That, I think, is the design difference at play here.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Note&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;s3tables:CreateTable&lt;/code&gt; + &lt;code&gt;s3tables:PutTableData&lt;/code&gt; pair shown above is specifically the permission set for the &lt;code&gt;createTable&lt;/code&gt; operation as defined in the Iceberg REST API reference (&lt;code&gt;s3-tables-integrating-open-source.html&lt;/code&gt;). A separate official document on SQL semantics (&lt;a href="https://docs.aws.amazon.com/AmazonS3/latest/userguide/s3-tables-sql.html" rel="noopener noreferrer"&gt;https://docs.aws.amazon.com/AmazonS3/latest/userguide/s3-tables-sql.html&lt;/a&gt;) states that the &lt;code&gt;CreateTable&lt;/code&gt; API only creates an empty table inside the table bucket, and that setting the schema requires two more actions, &lt;code&gt;UpdateTableMetadataLocation&lt;/code&gt; and &lt;code&gt;GetTableMetadataLocation&lt;/code&gt;, for four in total.&lt;/p&gt;

&lt;p&gt;In other words, "the API call that creates the table" and "actually getting a usable table with a schema set" require different numbers of permissions. If you only look at the REST API's permission table and assume two actions are enough, you can get tripped up during schema setup.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h1&gt;
  
  
  The renameTable / namespaceExists Difference
&lt;/h1&gt;

&lt;p&gt;The next difference: &lt;code&gt;renameTable&lt;/code&gt; appears in S3 Tables' list of supported APIs but not in Glue's.&lt;/p&gt;

&lt;p&gt;Glue's official documentation has no independent &lt;code&gt;renameTable&lt;/code&gt; operation. Instead, a note on the AWS Glue extension API's (&lt;code&gt;/extensions/v1/...&lt;/code&gt;) &lt;code&gt;StartUpdateTableTransaction&lt;/code&gt; says: "A rename operation can also be performed through this API. In that case, the caller must also have &lt;code&gt;glue:CreateTable&lt;/code&gt; or the Lake Formation &lt;code&gt;CREATE_TABLE&lt;/code&gt; permission for the destination table." So on the Glue side, renaming isn't its own operation; it's treated as a kind of update transaction.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;namespaceExists&lt;/code&gt;, meanwhile, exists as its own operation on the S3 Tables side (&lt;code&gt;HEAD /v1/{prefix}/namespaces/{namespace}&lt;/code&gt;), but I couldn't find it anywhere in Glue's list of standard Iceberg REST Catalog API operations. Even though both implement the same Iceberg REST spec, the design decision of "what gets carved out as its own API" clearly differs.&lt;/p&gt;

&lt;h1&gt;
  
  
  The updateNamespaceProperties Difference
&lt;/h1&gt;

&lt;p&gt;&lt;code&gt;updateNamespaceProperties&lt;/code&gt; existing only on the Glue side is another difference worth calling out. Glue has a clearly defined &lt;code&gt;updateNamespaceProperties&lt;/code&gt; operation with the IAM action &lt;code&gt;glue:UpdateDatabase&lt;/code&gt;, but this operation doesn't exist at all in S3 Tables' list of operations. S3 Tables' documented limitations say "only the &lt;code&gt;owner&lt;/code&gt; property is supported for namespaces," but there's no corresponding independent operation listed in the official operations table.&lt;/p&gt;

&lt;p&gt;This might get added in a future update, but as things stand, it's another point of difference between the two.&lt;/p&gt;

&lt;h1&gt;
  
  
  The dropTable purge Behavior Difference
&lt;/h1&gt;

&lt;p&gt;According to the official documentation, S3 Tables' &lt;code&gt;dropTable&lt;/code&gt; returns a &lt;code&gt;400 Bad Request&lt;/code&gt; if you specify &lt;code&gt;purge=false&lt;/code&gt;, meaning purge must be true. Glue's &lt;code&gt;DeleteTable&lt;/code&gt; (for tables on S3), on the other hand, is documented as failing the operation and not deleting the data if you specify &lt;code&gt;purge=true&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Same "delete the table" operation, but the required value of &lt;code&gt;purge&lt;/code&gt; looks like it's flipped between the two according to the docs.&lt;/p&gt;

&lt;p&gt;This looked like a likely source of mistakes, so I decided to actually check it hands-on. Testing it turned up something interesting.&lt;/p&gt;

&lt;p&gt;I tried it first against the S3 Tables endpoint.&lt;/p&gt;

&lt;h3&gt;
  
  
  S3 Tables' purge Behavior
&lt;/h3&gt;

&lt;p&gt;I created a table, then called &lt;code&gt;dropTable&lt;/code&gt; with no parameters at all:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;python &lt;span class="nt"&gt;-m&lt;/span&gt; awscurl &lt;span class="nt"&gt;--service&lt;/span&gt; s3tables &lt;span class="nt"&gt;--region&lt;/span&gt; ap-northeast-1 &lt;span class="nt"&gt;--profile&lt;/span&gt; your-profile &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-X&lt;/span&gt; DELETE &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="s2"&gt;"https://s3tables.ap-northeast-1.amazonaws.com/iceberg/v1/&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;BUCKET_ARN_PATH&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;/namespaces/analytics/tables/purge_test"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Result (excerpt):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"error"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"code"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;400&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"message"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"DropTable operation failed. S3 Tables only supports dropping tables with purge enabled."&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"purge_enabled"&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;As documented, this returned 400, and the table stayed in the listing.&lt;/p&gt;

&lt;p&gt;Next, I re-ran it with the query parameter &lt;code&gt;purge=true&lt;/code&gt; and got the exact same error. It turns out that, per the Iceberg REST Catalog spec, the query parameter for specifying purge on &lt;code&gt;dropTable&lt;/code&gt; isn't &lt;code&gt;purge&lt;/code&gt;; it's &lt;code&gt;purgeRequested&lt;/code&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;python &lt;span class="nt"&gt;-m&lt;/span&gt; awscurl &lt;span class="nt"&gt;--service&lt;/span&gt; s3tables &lt;span class="nt"&gt;--region&lt;/span&gt; ap-northeast-1 &lt;span class="nt"&gt;--profile&lt;/span&gt; your-profile &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-X&lt;/span&gt; DELETE &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="s2"&gt;"https://s3tables.ap-northeast-1.amazonaws.com/iceberg/v1/&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;BUCKET_ARN_PATH&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;/namespaces/analytics/tables/purge_test?purgeRequested=true"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This one succeeded with no response body, and I confirmed via the listing that the table had actually been deleted.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Note&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The gist of "purge must be true" was correct per the documentation, but assuming the query parameter is named &lt;code&gt;purge&lt;/code&gt; will trip you up. It's actually &lt;code&gt;purgeRequested&lt;/code&gt;.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;If you use both endpoints together, this alone is fertile ground for implementation mistakes.&lt;/p&gt;

&lt;h3&gt;
  
  
  Glue Data Catalog's purge Behavior
&lt;/h3&gt;

&lt;p&gt;I then ran the same test on the Glue side.&lt;/p&gt;

&lt;p&gt;My test environment has the Glue endpoint reaching S3 Tables-backed tables through the &lt;code&gt;s3tablescatalog&lt;/code&gt; federated catalog. So I first called &lt;code&gt;DeleteTable&lt;/code&gt; against this table via the Glue endpoint with &lt;code&gt;purgeRequested=true&lt;/code&gt; specified, and it succeeded without any fuss.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;python &lt;span class="nt"&gt;-m&lt;/span&gt; awscurl &lt;span class="nt"&gt;--service&lt;/span&gt; glue &lt;span class="nt"&gt;--region&lt;/span&gt; ap-northeast-1 &lt;span class="nt"&gt;--profile&lt;/span&gt; your-profile &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-X&lt;/span&gt; DELETE &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="s2"&gt;"https://glue.ap-northeast-1.amazonaws.com/iceberg/v1/catalogs/123456789012:s3tablescatalog:penguin-rest-test/namespaces/analytics/tables/purge_test_glue?purgeRequested=true"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This contradicts the official documentation's claim that "Glue's &lt;code&gt;DeleteTable&lt;/code&gt; fails if &lt;code&gt;purge=true&lt;/code&gt;." So I set up another table and tried deleting it via Glue without specifying &lt;code&gt;purgeRequested&lt;/code&gt; at all.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;python &lt;span class="nt"&gt;-m&lt;/span&gt; awscurl &lt;span class="nt"&gt;--service&lt;/span&gt; glue &lt;span class="nt"&gt;--region&lt;/span&gt; ap-northeast-1 &lt;span class="nt"&gt;--profile&lt;/span&gt; your-profile &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-X&lt;/span&gt; DELETE &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="s2"&gt;"https://glue.ap-northeast-1.amazonaws.com/iceberg/v1/catalogs/123456789012:s3tablescatalog:penguin-rest-test/namespaces/analytics/tables/purge_test_glue2"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Result (excerpt):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"error"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"code"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;400&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"message"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"PurgeRequested must be true for S3 federated iceberg tables."&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"InvalidInputException"&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The error message spells it out clearly: "S3 federated iceberg tables." So the Glue endpoint internally distinguishes whether a table's underlying entity is S3 Tables-backed (federated), and in that case it demands &lt;code&gt;purgeRequested=true&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;This tells us that the &lt;code&gt;purge&lt;/code&gt; behavior of &lt;code&gt;DeleteTable&lt;/code&gt; isn't determined by which endpoint you're hitting (Glue or S3 Tables). What determines it is the underlying entity of the table: whether the backend is an S3 Tables-federated table or a plain table on a general-purpose S3 bucket.&lt;/p&gt;

&lt;p&gt;The documentation's statement that "Glue's &lt;code&gt;DeleteTable&lt;/code&gt; fails for S3-backed tables if &lt;code&gt;purge=true&lt;/code&gt;" is presumably describing a genuinely Glue-native Iceberg table on a general-purpose S3 bucket. For a table backed by S3 Tables, even when accessed through Glue, S3 Tables' own constraint (&lt;code&gt;purgeRequested=true&lt;/code&gt; required) appears to pass straight through.&lt;/p&gt;

&lt;p&gt;To test this hypothesis, I created an Iceberg table on a general-purpose S3 bucket via Athena and ran the same test through the Glue endpoint.&lt;/p&gt;

&lt;p&gt;First, deleting without specifying &lt;code&gt;purgeRequested&lt;/code&gt; succeeded:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;python &lt;span class="nt"&gt;-m&lt;/span&gt; awscurl &lt;span class="nt"&gt;--service&lt;/span&gt; glue &lt;span class="nt"&gt;--region&lt;/span&gt; ap-northeast-1 &lt;span class="nt"&gt;--profile&lt;/span&gt; your-profile &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-X&lt;/span&gt; DELETE &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="s2"&gt;"https://glue.ap-northeast-1.amazonaws.com/iceberg/v1/catalogs/123456789012/namespaces/general_iceberg_db/tables/purge_test_general"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then, on a second table, specifying &lt;code&gt;purgeRequested=true&lt;/code&gt; failed:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;python &lt;span class="nt"&gt;-m&lt;/span&gt; awscurl &lt;span class="nt"&gt;--service&lt;/span&gt; glue &lt;span class="nt"&gt;--region&lt;/span&gt; ap-northeast-1 &lt;span class="nt"&gt;--profile&lt;/span&gt; your-profile &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-X&lt;/span&gt; DELETE &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="s2"&gt;"https://glue.ap-northeast-1.amazonaws.com/iceberg/v1/catalogs/123456789012/namespaces/general_iceberg_db/tables/purge_test_general2?purgeRequested=true"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Result (excerpt):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"error"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"code"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;400&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"message"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"PurgeRequested cannot be true for Glue iceberg tables."&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"InvalidInputException"&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This mirrors the earlier error for the S3 Tables-backed table (&lt;code&gt;PurgeRequested must be true for S3 federated iceberg tables.&lt;/code&gt;) almost perfectly. AWS itself clearly distinguishes "Glue iceberg tables" from "S3 federated iceberg tables" in its code, and imposes opposite &lt;code&gt;purgeRequested&lt;/code&gt; requirements on each.&lt;/p&gt;

&lt;h3&gt;
  
  
  Summary of purge Behavior for Each
&lt;/h3&gt;

&lt;p&gt;Here's the full result across all four cases tested:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Table entity&lt;/th&gt;
&lt;th&gt;Without &lt;code&gt;purgeRequested&lt;/code&gt;
&lt;/th&gt;
&lt;th&gt;With &lt;code&gt;purgeRequested=true&lt;/code&gt;
&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;S3 Tables-backed (via Glue endpoint)&lt;/td&gt;
&lt;td&gt;400 "PurgeRequested must be true for S3 federated iceberg tables"&lt;/td&gt;
&lt;td&gt;Success&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;S3 Tables-backed (direct S3 Tables endpoint)&lt;/td&gt;
&lt;td&gt;400 "S3 Tables only supports dropping tables with purge enabled"&lt;/td&gt;
&lt;td&gt;Success&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;General-purpose S3, Glue-native table (via Glue endpoint)&lt;/td&gt;
&lt;td&gt;Success&lt;/td&gt;
&lt;td&gt;400 "PurgeRequested cannot be true for Glue iceberg tables"&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Both failure cases without &lt;code&gt;purgeRequested&lt;/code&gt; return the same status code, 400. The nature of the error is consistent regardless of which endpoint you hit; the only difference is what the message says caused the failure.&lt;/p&gt;

&lt;p&gt;It's not the entry point (Glue endpoint vs. S3 Tables endpoint) that decides the &lt;code&gt;purgeRequested&lt;/code&gt; requirement; it's the underlying table entity (Glue Iceberg Tables vs. S3 Federated Iceberg Tables). Both the hands-on results and the error messages back this up.&lt;/p&gt;

&lt;p&gt;I'd honestly prefer they picked one convention and stuck with it...&lt;/p&gt;

&lt;h1&gt;
  
  
  CloudTrail Granularity Also Reflects the API Design Split
&lt;/h1&gt;

&lt;p&gt;S3 Tables' official documentation states clearly that "&lt;code&gt;LoadTable&lt;/code&gt; generates a &lt;code&gt;GetTableMetadataLocation&lt;/code&gt; management event and a &lt;code&gt;GetTableData&lt;/code&gt; data event." This lines up with the IAM action split described above. S3 Tables appears to be designed to separate "metadata" from "the data body" at the audit-log level too.&lt;/p&gt;

&lt;p&gt;On the Glue side, the official documentation shows each operation's CloudTrail event name matching its IAM action name (&lt;code&gt;glue:GetTable&lt;/code&gt; logs as &lt;code&gt;glue:GetTable&lt;/code&gt;), with no split like the one seen on S3 Tables. That said, as I confirmed hands-on in a previous article, when the Glue endpoint calls Lake Formation's &lt;code&gt;GetDataAccess&lt;/code&gt;, that gets logged separately as its own event. It seems Glue's audit trail is split across two distinct lineages: "the log of the API operation itself" and "the Lake Formation authorization log."&lt;/p&gt;

&lt;h1&gt;
  
  
  Conclusion
&lt;/h1&gt;

&lt;p&gt;This time, taking a different approach from the hands-on investigations I've done so far, I laid the official documentation-based permission mapping tables for Glue Data Catalog's and S3 Tables' respective Iceberg REST APIs side by side.&lt;/p&gt;

&lt;p&gt;To summarize:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Granularity of data-operation permissions&lt;/strong&gt;: Glue consolidates each operation into a single IAM action, while S3 Tables splits metadata and data-body actions apart (&lt;code&gt;createTable&lt;/code&gt; needs only two actions as a REST operation, but per the SQL semantics documentation, actually getting a usable table with a schema set requires four).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Rename operations&lt;/strong&gt;: Glue has no independent API for this; it's handled as part of an update transaction. S3 Tables has its own dedicated &lt;code&gt;renameTable&lt;/code&gt; operation.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;namespaceExists&lt;/strong&gt;: exists on the S3 Tables side but not in Glue's standard API list.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;updateNamespaceProperties&lt;/strong&gt;: exists on the Glue side but has no corresponding operation on S3 Tables (only the &lt;code&gt;owner&lt;/code&gt; property is supported, in a limited way).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;dropTable's purge behavior&lt;/strong&gt;: the query parameter is &lt;code&gt;purgeRequested&lt;/code&gt;, not &lt;code&gt;purge&lt;/code&gt;. Whether purge is required turns out to be determined not by "Glue endpoint vs. S3 Tables endpoint" but by the table's underlying entity (S3 Tables-federated vs. Glue-native on general-purpose S3), and the requirement is the exact opposite in each case, which I confirmed hands-on across all four scenarios.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;CloudTrail granularity&lt;/strong&gt;: S3 Tables logs separate events for metadata operations and data operations, while Glue logs one event per operation, with Lake Formation evaluation showing up as a separate lineage.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Even though both implement the same Iceberg REST Catalog spec, it turns out the granularity of the permission model and the design decisions around what gets carved out as an independent API differ quite a bit between Glue and S3 Tables. I think this is the same structural difference I touched on in earlier articles, Glue as a catalog hub and S3 Tables as a thin translation layer over storage, showing up consistently even at the level of permission mapping tables.&lt;/p&gt;

&lt;p&gt;Comparing hands-on results against the documentation turns up things you'd miss looking at either one alone, so I'd like to keep doing this kind of exercise going forward.&lt;/p&gt;

&lt;p&gt;I hope this article is useful to anyone choosing between the two endpoints.&lt;/p&gt;

</description>
      <category>aws</category>
      <category>dataengineering</category>
    </item>
    <item>
      <title>Connecting Amazon S3 Tables with Snowflake</title>
      <dc:creator>Aki</dc:creator>
      <pubDate>Tue, 21 Jul 2026 00:06:40 +0000</pubDate>
      <link>https://dev.to/aws-builders/connecting-amazon-s3-tables-with-snowflake-29n6</link>
      <guid>https://dev.to/aws-builders/connecting-amazon-s3-tables-with-snowflake-29n6</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Original Japanese article&lt;/strong&gt;: &lt;a href="https://zenn.dev/penginpenguin/articles/d9c916abc833f2" rel="noopener noreferrer"&gt;Amazon S3 TablesとSnowflakeを連携する&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h1&gt;
  
  
  Introduction
&lt;/h1&gt;

&lt;p&gt;I'm Aki, an AWS Community Builder (&lt;a href="https://x.com/jitepengin" rel="noopener noreferrer"&gt;@jitepengin&lt;/a&gt;).&lt;/p&gt;

&lt;p&gt;In a previous article, I introduced how to connect an Iceberg table built on a general purpose S3 bucket with Snowflake.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://dev.to/aws-builders/aws-snowflake-lakehouse-2-practical-apache-iceberg-integration-patterns-812"&gt;AWS Snowflake Lakehouse: 2 Practical Apache Iceberg Integration Patterns&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;This time, as a follow up, I'm going to read and write an Iceberg table on S3 Tables from Snowflake, since I've had more chances to use S3 Tables recently.&lt;/p&gt;

&lt;p&gt;There were a few points that tripped me up along the way, so I put this together along with some personal reflections. Hope it's useful!&lt;/p&gt;

&lt;h1&gt;
  
  
  Test Environment
&lt;/h1&gt;

&lt;p&gt;I'm reusing the same resources as in the previous article.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://dev.to/aws-builders/verifying-how-iam-and-lake-formation-behave-for-the-glue-rest-catalog-and-s3-tables-2cgc"&gt;Verifying How IAM and Lake Formation Behave for the Glue REST Catalog and S3 Tables&lt;/a&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Table bucket: &lt;code&gt;penguin-rest-test&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Namespace: &lt;code&gt;analytics&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Table: &lt;code&gt;daily_sales&lt;/code&gt; (two columns, &lt;code&gt;sales_date&lt;/code&gt; and &lt;code&gt;amount&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;Glue integration: enabled (mounted as &lt;code&gt;s3tablescatalog/penguin-rest-test&lt;/code&gt;)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Read the account ID as &lt;code&gt;123456789012&lt;/code&gt; throughout.&lt;/p&gt;

&lt;h1&gt;
  
  
  Reading S3 Tables Data from Snowflake
&lt;/h1&gt;

&lt;h2&gt;
  
  
  Creating the IAM Role
&lt;/h2&gt;

&lt;p&gt;I'll create a role that lets Snowflake access S3 Tables through the Glue Iceberg REST endpoint.&lt;/p&gt;

&lt;p&gt;I'm reusing the IAM actions from the previous article that were required for access through the Glue endpoint (&lt;code&gt;s3tables:*&lt;/code&gt;, &lt;code&gt;lakeformation:GetDataAccess&lt;/code&gt;, and the &lt;code&gt;glue:GetCatalog&lt;/code&gt; family), and defining them as the policy for the catalog integration role.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"Version"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"2012-10-17"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"Statement"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="nl"&gt;"Sid"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"S3TablesAccess"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="nl"&gt;"Effect"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Allow"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="nl"&gt;"Action"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
                &lt;/span&gt;&lt;span class="s2"&gt;"s3tables:ListTableBuckets"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
                &lt;/span&gt;&lt;span class="s2"&gt;"s3tables:GetTableBucket"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
                &lt;/span&gt;&lt;span class="s2"&gt;"s3tables:GetNamespace"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
                &lt;/span&gt;&lt;span class="s2"&gt;"s3tables:ListNamespaces"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
                &lt;/span&gt;&lt;span class="s2"&gt;"s3tables:GetTable"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
                &lt;/span&gt;&lt;span class="s2"&gt;"s3tables:ListTables"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
                &lt;/span&gt;&lt;span class="s2"&gt;"s3tables:GetTableMetadataLocation"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
                &lt;/span&gt;&lt;span class="s2"&gt;"s3tables:UpdateTableMetadataLocation"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
                &lt;/span&gt;&lt;span class="s2"&gt;"s3tables:GetTableData"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
                &lt;/span&gt;&lt;span class="s2"&gt;"s3tables:PutTableData"&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="nl"&gt;"Resource"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"*"&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="nl"&gt;"Sid"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"LakeFormationAccess"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="nl"&gt;"Effect"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Allow"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="nl"&gt;"Action"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"lakeformation:GetDataAccess"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="nl"&gt;"Resource"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"*"&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="nl"&gt;"Sid"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"GlueS3TablesCatalogAccess"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="nl"&gt;"Effect"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Allow"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="nl"&gt;"Action"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
                &lt;/span&gt;&lt;span class="s2"&gt;"glue:GetCatalog"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
                &lt;/span&gt;&lt;span class="s2"&gt;"glue:GetCatalogs"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
                &lt;/span&gt;&lt;span class="s2"&gt;"glue:GetPartitions"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
                &lt;/span&gt;&lt;span class="s2"&gt;"glue:GetPartition"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
                &lt;/span&gt;&lt;span class="s2"&gt;"glue:GetDatabase"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
                &lt;/span&gt;&lt;span class="s2"&gt;"glue:GetDatabases"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
                &lt;/span&gt;&lt;span class="s2"&gt;"glue:GetTable"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
                &lt;/span&gt;&lt;span class="s2"&gt;"glue:GetTables"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
                &lt;/span&gt;&lt;span class="s2"&gt;"glue:UpdateTable"&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="nl"&gt;"Resource"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
                &lt;/span&gt;&lt;span class="s2"&gt;"arn:aws:glue:*:123456789012:catalog"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
                &lt;/span&gt;&lt;span class="s2"&gt;"arn:aws:glue:*:123456789012:catalog/*"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
                &lt;/span&gt;&lt;span class="s2"&gt;"arn:aws:glue:*:123456789012:database/*"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
                &lt;/span&gt;&lt;span class="s2"&gt;"arn:aws:glue:*:123456789012:table/*/*"&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Alongside the read actions (&lt;code&gt;GetTable&lt;/code&gt;, &lt;code&gt;GetTableData&lt;/code&gt;, and so on), I've also included the write actions &lt;code&gt;s3tables:PutTableData&lt;/code&gt; and &lt;code&gt;glue:UpdateTable&lt;/code&gt; up front. Granting write permissions here means writes will also be possible once the integration is set up.&lt;/p&gt;

&lt;h2&gt;
  
  
  Creating the CATALOG INTEGRATION
&lt;/h2&gt;

&lt;p&gt;On the Snowflake side, I'll create a catalog integration that connects to the Iceberg REST Catalog (AWS's Glue implementation).&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;OR&lt;/span&gt; &lt;span class="k"&gt;REPLACE&lt;/span&gt; &lt;span class="k"&gt;CATALOG&lt;/span&gt; &lt;span class="n"&gt;INTEGRATION&lt;/span&gt; &lt;span class="n"&gt;penguin_catalog_integration&lt;/span&gt;
  &lt;span class="n"&gt;CATALOG_SOURCE&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;ICEBERG_REST&lt;/span&gt;
  &lt;span class="n"&gt;TABLE_FORMAT&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;ICEBERG&lt;/span&gt;
  &lt;span class="n"&gt;CATALOG_NAMESPACE&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'analytics'&lt;/span&gt;
  &lt;span class="n"&gt;REST_CONFIG&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;CATALOG_URI&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'https://glue.ap-northeast-1.amazonaws.com/iceberg'&lt;/span&gt;
    &lt;span class="n"&gt;CATALOG_API_TYPE&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;AWS_GLUE&lt;/span&gt;
    &lt;span class="k"&gt;CATALOG_NAME&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'123456789012:s3tablescatalog/penguin-rest-test'&lt;/span&gt;
    &lt;span class="n"&gt;ACCESS_DELEGATION_MODE&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;VENDED_CREDENTIALS&lt;/span&gt;
  &lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="n"&gt;REST_AUTHENTICATION&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="k"&gt;TYPE&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;SIGV4&lt;/span&gt;
    &lt;span class="n"&gt;SIGV4_IAM_ROLE&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'arn:aws:iam::123456789012:role/Snowflake_Catalog_Integration'&lt;/span&gt;
    &lt;span class="n"&gt;SIGV4_SIGNING_REGION&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'ap-northeast-1'&lt;/span&gt;
  &lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="n"&gt;ENABLED&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;TRUE&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You can see that &lt;code&gt;CATALOG_URI&lt;/code&gt; is the exact same Glue Iceberg REST endpoint I was hitting directly in the previous article.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;CATALOG_NAME&lt;/code&gt; takes the nested catalog's prefix notation (&lt;code&gt;account ID:s3tablescatalog/bucket name&lt;/code&gt;). This mixes colon and slash separators, matching the same &lt;code&gt;--catalog-id&lt;/code&gt; format I confirmed against Glue's native API in the previous article.&lt;/p&gt;

&lt;p&gt;Since I've specified &lt;code&gt;ACCESS_DELEGATION_MODE = VENDED_CREDENTIALS&lt;/code&gt;, Snowflake operates by receiving vended credentials issued from the Glue endpoint.&lt;/p&gt;

&lt;h2&gt;
  
  
  DESC CATALOG INTEGRATION
&lt;/h2&gt;

&lt;p&gt;Once you create the catalog integration, Snowflake issues an IAM user ARN and an external ID for it to assume on the AWS side.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;DESC&lt;/span&gt; &lt;span class="k"&gt;CATALOG&lt;/span&gt; &lt;span class="n"&gt;INTEGRATION&lt;/span&gt; &lt;span class="n"&gt;penguin_catalog_integration&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Two properties, &lt;code&gt;API_AWS_IAM_USER_ARN&lt;/code&gt; and &lt;code&gt;API_AWS_EXTERNAL_ID&lt;/code&gt;, come back in the result. Keep these on hand.&lt;/p&gt;

&lt;h2&gt;
  
  
  Setting Up the Trust Policy
&lt;/h2&gt;

&lt;p&gt;On the trust policy of the role specified in &lt;code&gt;SIGV4_IAM_ROLE&lt;/code&gt; (&lt;code&gt;Snowflake_Catalog_Integration&lt;/code&gt;), allow the Snowflake side IAM user obtained from &lt;code&gt;DESC CATALOG INTEGRATION&lt;/code&gt; above, along with its external ID.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"Version"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"2012-10-17"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"Statement"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="nl"&gt;"Effect"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Allow"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="nl"&gt;"Principal"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
                &lt;/span&gt;&lt;span class="nl"&gt;"AWS"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"arn:aws:iam::999999999999:user/xxxx1000-s"&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="nl"&gt;"Action"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"sts:AssumeRole"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="nl"&gt;"Condition"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
                &lt;/span&gt;&lt;span class="nl"&gt;"StringEquals"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
                    &lt;/span&gt;&lt;span class="nl"&gt;"sts:ExternalId"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"SFCRole=2_(masked)"&lt;/span&gt;&lt;span class="w"&gt;
                &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;Principal&lt;/code&gt; is an IAM user that belongs to an AWS account managed by Snowflake. Attaching the &lt;code&gt;sts:ExternalId&lt;/code&gt; condition prevents AssumeRole calls from anything other than this specific catalog integration on Snowflake's side. At this point, Snowflake can assume the role and use the IAM side permissions.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Note&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The IAM policy body and the trust policy are two separate things. Even if you've included &lt;code&gt;s3tables:PutTableData&lt;/code&gt; in the IAM policy, if the trust policy isn't set up correctly, the role can't be assumed and the catalog integration won't work. Check both.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Registering the Lake Formation Data Location
&lt;/h2&gt;

&lt;p&gt;Separately from the trust policy, you also need to register this table bucket as a data lake location on the Lake Formation side.&lt;/p&gt;

&lt;p&gt;This is the same idea as &lt;code&gt;register-resource --with-federation&lt;/code&gt; that I covered in a previous article. Without this step, Lake Formation can't perform permission evaluation or issue vended credentials for this resource in the first place.&lt;/p&gt;

&lt;p&gt;Registration requires an IAM role (a data access role) that can actually reach the target S3 location. This time I created a role named &lt;code&gt;penguin-lakeformation-role&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The permission policy grants metadata reads via Glue, plus full access to S3 Tables tables and namespaces (read/write, along with Create/Delete/Rename operations).&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"Version"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"2012-10-17"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"Statement"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="nl"&gt;"Effect"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Allow"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="nl"&gt;"Action"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
                &lt;/span&gt;&lt;span class="s2"&gt;"glue:GetCatalog"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
                &lt;/span&gt;&lt;span class="s2"&gt;"glue:GetDatabase"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
                &lt;/span&gt;&lt;span class="s2"&gt;"glue:GetDatabases"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
                &lt;/span&gt;&lt;span class="s2"&gt;"glue:GetTable"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
                &lt;/span&gt;&lt;span class="s2"&gt;"glue:GetTables"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
                &lt;/span&gt;&lt;span class="s2"&gt;"glue:GetPartitions"&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="nl"&gt;"Resource"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"*"&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="nl"&gt;"Sid"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"ListBuckets"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="nl"&gt;"Effect"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Allow"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="nl"&gt;"Action"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"s3tables:ListTableBuckets"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="nl"&gt;"Resource"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"*"&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="nl"&gt;"Effect"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Allow"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="nl"&gt;"Action"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
                &lt;/span&gt;&lt;span class="s2"&gt;"s3tables:GetTableBucket"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
                &lt;/span&gt;&lt;span class="s2"&gt;"s3tables:GetNamespace"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
                &lt;/span&gt;&lt;span class="s2"&gt;"s3tables:ListNamespaces"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
                &lt;/span&gt;&lt;span class="s2"&gt;"s3tables:GetTable"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
                &lt;/span&gt;&lt;span class="s2"&gt;"s3tables:ListTables"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
                &lt;/span&gt;&lt;span class="s2"&gt;"s3tables:GetTableMetadataLocation"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
                &lt;/span&gt;&lt;span class="s2"&gt;"s3tables:UpdateTableMetadataLocation"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
                &lt;/span&gt;&lt;span class="s2"&gt;"s3tables:GetTableData"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
                &lt;/span&gt;&lt;span class="s2"&gt;"s3tables:PutTableData"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
                &lt;/span&gt;&lt;span class="s2"&gt;"s3tables:CreateTable"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
                &lt;/span&gt;&lt;span class="s2"&gt;"s3tables:DeleteTable"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
                &lt;/span&gt;&lt;span class="s2"&gt;"s3tables:RenameTable"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
                &lt;/span&gt;&lt;span class="s2"&gt;"s3tables:CreateNamespace"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
                &lt;/span&gt;&lt;span class="s2"&gt;"s3tables:DeleteNamespace"&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="nl"&gt;"Resource"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
                &lt;/span&gt;&lt;span class="s2"&gt;"arn:aws:s3tables:ap-northeast-1:123456789012:bucket/*"&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The trust policy allows AssumeRole from &lt;code&gt;lakeformation.amazonaws.com&lt;/code&gt;, but it turned out I needed to allow not just &lt;code&gt;sts:AssumeRole&lt;/code&gt; but also &lt;code&gt;sts:SetSourceIdentity&lt;/code&gt; and &lt;code&gt;sts:SetContext&lt;/code&gt; alongside it. I've also restricted calls to my own account with the &lt;code&gt;aws:SourceAccount&lt;/code&gt; condition.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"Version"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"2012-10-17"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"Statement"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="nl"&gt;"Effect"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Allow"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="nl"&gt;"Principal"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
                &lt;/span&gt;&lt;span class="nl"&gt;"Service"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"lakeformation.amazonaws.com"&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="nl"&gt;"Action"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
                &lt;/span&gt;&lt;span class="s2"&gt;"sts:AssumeRole"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
                &lt;/span&gt;&lt;span class="s2"&gt;"sts:SetSourceIdentity"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
                &lt;/span&gt;&lt;span class="s2"&gt;"sts:SetContext"&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="nl"&gt;"Condition"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
                &lt;/span&gt;&lt;span class="nl"&gt;"StringEquals"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
                    &lt;/span&gt;&lt;span class="nl"&gt;"aws:SourceAccount"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"123456789012"&lt;/span&gt;&lt;span class="w"&gt;
                &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;With this role specified, register the table bucket as a federated Lake Formation resource.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;aws lakeformation register-resource &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--resource-arn&lt;/span&gt; &lt;span class="s2"&gt;"arn:aws:s3tables:ap-northeast-1:123456789012:bucket/*"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--with-federation&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--with-privileged-access&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--role-arn&lt;/span&gt; arn:aws:iam::123456789012:role/penguin-lakeformation-role &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--region&lt;/span&gt; ap-northeast-1
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Specifying a wildcard (&lt;code&gt;bucket/*&lt;/code&gt;) for &lt;code&gt;--resource-arn&lt;/code&gt; also lets you skip having to re-register each table bucket individually.&lt;/p&gt;

&lt;p&gt;Note: if you want more flexible control, such as registering table buckets individually with different settings per bucket, you'll need to specify each one separately.&lt;/p&gt;

&lt;p&gt;If this location registration is missing, permission evaluation won't work correctly at actual access time, even if the IAM policy, trust policy, and Lake Formation grants are all set up correctly. By the way, S3 Tables location registration can't be done from the console, so command line operation is required here.&lt;/p&gt;

&lt;p&gt;Note: I really wish this could be registered and edited from the console...&lt;/p&gt;

&lt;h2&gt;
  
  
  Lake Formation Grants (Three Tiers: Catalog, Database, Table)
&lt;/h2&gt;

&lt;p&gt;Grant permissions to the &lt;code&gt;Snowflake_Catalog_Integration&lt;/code&gt; role at each of the three tiers, Catalog, Database, and Table.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Catalog tier&lt;/span&gt;
aws lakeformation grant-permissions &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--principal&lt;/span&gt; &lt;span class="nv"&gt;DataLakePrincipalIdentifier&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;arn:aws:iam::123456789012:role/Snowflake_Catalog_Integration &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--resource&lt;/span&gt; &lt;span class="s1"&gt;'{"Catalog":{"Id":"123456789012:s3tablescatalog/penguin-rest-test"}}'&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--permissions&lt;/span&gt; &lt;span class="s2"&gt;"ALL DESCRIBE"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--permissions-with-grant-option&lt;/span&gt; &lt;span class="s2"&gt;"ALL DESCRIBE"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--region&lt;/span&gt; ap-northeast-1

&lt;span class="c"&gt;# Database tier&lt;/span&gt;
aws lakeformation grant-permissions &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--principal&lt;/span&gt; &lt;span class="nv"&gt;DataLakePrincipalIdentifier&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;arn:aws:iam::123456789012:role/Snowflake_Catalog_Integration &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--resource&lt;/span&gt; &lt;span class="s1"&gt;'{"Database":{"CatalogId":"123456789012:s3tablescatalog/penguin-rest-test","Name":"analytics"}}'&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--permissions&lt;/span&gt; &lt;span class="s2"&gt;"ALL CREATE_TABLE DESCRIBE"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--permissions-with-grant-option&lt;/span&gt; ALL CREATE_TABLE DESCRIBE &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--region&lt;/span&gt; ap-northeast-1

&lt;span class="c"&gt;# Table tier&lt;/span&gt;
aws lakeformation grant-permissions &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--principal&lt;/span&gt; &lt;span class="nv"&gt;DataLakePrincipalIdentifier&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;arn:aws:iam::123456789012:role/Snowflake_Catalog_Integration &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--resource&lt;/span&gt; &lt;span class="s1"&gt;'{"Table":{"CatalogId":"123456789012:s3tablescatalog/penguin-rest-test","DatabaseName":"analytics","Name":"daily_sales"}}'&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--permissions&lt;/span&gt; &lt;span class="s2"&gt;"ALL"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--permissions-with-grant-option&lt;/span&gt; ALL &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--region&lt;/span&gt; ap-northeast-1
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  CREATE OR REPLACE ICEBERG TABLE
&lt;/h2&gt;

&lt;p&gt;With the catalog integration ready, register the Iceberg table individually on the Snowflake side.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;OR&lt;/span&gt; &lt;span class="k"&gt;REPLACE&lt;/span&gt; &lt;span class="n"&gt;ICEBERG&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;GOLD&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;GOLD&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;s3tables_table&lt;/span&gt;
  &lt;span class="k"&gt;CATALOG&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'penguin_catalog_integration'&lt;/span&gt;
  &lt;span class="n"&gt;CATALOG_NAMESPACE&lt;/span&gt;  &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'analytics'&lt;/span&gt;
  &lt;span class="n"&gt;CATALOG_TABLE_NAME&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'daily_sales'&lt;/span&gt;
  &lt;span class="n"&gt;AUTO_REFRESH&lt;/span&gt;       &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;TRUE&lt;/span&gt;
&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;CATALOG_NAMESPACE&lt;/code&gt; and &lt;code&gt;CATALOG_TABLE_NAME&lt;/code&gt; explicitly specify the namespace and table name on the S3 Tables side. This is the key point of this approach: you have to run this statement once for every single table you register. When a new table shows up on the S3 Tables side, it doesn't automatically appear on the Snowflake side.&lt;/p&gt;

&lt;h2&gt;
  
  
  Verification
&lt;/h2&gt;

&lt;p&gt;I'll insert data into the Iceberg table from the Athena side and check whether it shows up from Snowflake.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="c1"&gt;-- Athena side&lt;/span&gt;
&lt;span class="k"&gt;INSERT&lt;/span&gt; &lt;span class="k"&gt;INTO&lt;/span&gt; &lt;span class="nv"&gt;"analytics"&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nv"&gt;"daily_sales"&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;sales_date&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;amount&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;VALUES&lt;/span&gt;
    &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;CAST&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'2025-01-01'&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="nb"&gt;DATE&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="k"&gt;CAST&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;10000&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="nb"&gt;BIGINT&lt;/span&gt;&lt;span class="p"&gt;)),&lt;/span&gt;
    &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;CAST&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'2025-01-02'&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="nb"&gt;DATE&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="k"&gt;CAST&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;15000&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="nb"&gt;BIGINT&lt;/span&gt;&lt;span class="p"&gt;)),&lt;/span&gt;
    &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;CAST&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'2025-01-03'&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="nb"&gt;DATE&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="k"&gt;CAST&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;12000&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="nb"&gt;BIGINT&lt;/span&gt;&lt;span class="p"&gt;)),&lt;/span&gt;
    &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;CAST&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'2025-01-04'&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="nb"&gt;DATE&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="k"&gt;CAST&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;18000&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="nb"&gt;BIGINT&lt;/span&gt;&lt;span class="p"&gt;)),&lt;/span&gt;
    &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;CAST&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'2025-01-05'&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="nb"&gt;DATE&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="k"&gt;CAST&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;20000&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="nb"&gt;BIGINT&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;After refreshing the table on the Snowflake side and running a SELECT, the data I'd inserted from Athena showed up without any issues!&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;ALTER&lt;/span&gt; &lt;span class="n"&gt;ICEBERG&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;GOLD&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;GOLD&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;s3tables_table&lt;/span&gt; &lt;span class="n"&gt;REFRESH&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;GOLD&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;GOLD&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;s3tables_table&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F54g4mifj6moj99lh4f7t.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F54g4mifj6moj99lh4f7t.png" alt="The SELECT result on Snowflake showing the data inserted from Athena" width="800" height="301"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h1&gt;
  
  
  Using a Catalog-Linked Database
&lt;/h1&gt;

&lt;p&gt;With the &lt;code&gt;CREATE ICEBERG TABLE&lt;/code&gt; approach, I had to register tables one at a time by hand. Using a Catalog-Linked Database (CLD) lets you link an entire namespace (database) at once instead.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;OR&lt;/span&gt; &lt;span class="k"&gt;REPLACE&lt;/span&gt; &lt;span class="k"&gt;DATABASE&lt;/span&gt; &lt;span class="n"&gt;LINKED_DB&lt;/span&gt;
  &lt;span class="n"&gt;LINKED_CATALOG&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="k"&gt;CATALOG&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'penguin_catalog_integration'&lt;/span&gt;
  &lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The moment I created it, &lt;code&gt;daily_sales&lt;/code&gt;, which already existed under the &lt;code&gt;analytics&lt;/code&gt; namespace, showed up in the table list automatically, with no DDL needed.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F5n4ogyqfqchn5yl6xe84.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F5n4ogyqfqchn5yl6xe84.png" alt="daily_sales being automatically detected right after LINKED_DB is created" width="800" height="335"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Verification (Auto Detection)
&lt;/h2&gt;

&lt;p&gt;To confirm that auto detection actually works, let's create a new table from the AWS side.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="c1"&gt;-- Athena side&lt;/span&gt;
&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="nv"&gt;`analytics`&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;daily_sales2&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="n"&gt;sale_date&lt;/span&gt; &lt;span class="nb"&gt;date&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;product_category&lt;/span&gt; &lt;span class="n"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;sales_amount&lt;/span&gt; &lt;span class="nb"&gt;double&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;PARTITIONED&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;month&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;sale_date&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;span class="n"&gt;TBLPROPERTIES&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'table_type'&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'iceberg'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fw19cxpukpsuv20q0ttef.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fw19cxpukpsuv20q0ttef.png" alt="Athena query editor screen creating daily_sales2" width="800" height="268"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;After the CREATE, checking Snowsight showed the newly created table had been added automatically!&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fxd6qwo5ukve6407k33j3.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fxd6qwo5ukve6407k33j3.png" alt="LINKED_DB.analytics showing both daily_sales and daily_sales2" width="799" height="342"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Unlike the CREATE ICEBERG TABLE approach, there's no need to write DDL for each table. I confirmed that changes on the AWS side are reflected directly on the Snowflake side.&lt;/p&gt;

&lt;h2&gt;
  
  
  Writing from Snowflake
&lt;/h2&gt;

&lt;p&gt;Up to this point, data had only flowed one way, from AWS to Snowflake. Let's also check whether data inserted from the Snowflake side shows up on the AWS side (Athena).&lt;/p&gt;

&lt;h3&gt;
  
  
  The Individually Added Table
&lt;/h3&gt;

&lt;p&gt;First, let's write to the table that was added individually with CREATE ICEBERG TABLE.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;INSERT&lt;/span&gt; &lt;span class="k"&gt;INTO&lt;/span&gt; &lt;span class="n"&gt;GOLD&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;GOLD&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;S3TABLES_TABLE&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;SALES_DATE&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;AMOUNT&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;VALUES&lt;/span&gt;
    &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'2026-07-16'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;10000&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'2026-07-16'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;15000&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'2026-07-16'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;12000&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fzpmge2pluqm8xjf119vr.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fzpmge2pluqm8xjf119vr.png" alt="Inserting into the individually added table from Snowflake, 3 rows inserted" width="800" height="301"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Let's check on the AWS side (Athena).&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="nv"&gt;"analytics"&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nv"&gt;"daily_sales"&lt;/span&gt; &lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;sales_date&lt;/span&gt; &lt;span class="k"&gt;DESC&lt;/span&gt; &lt;span class="k"&gt;limit&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fw4kf1eygd7xiwems92xc.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fw4kf1eygd7xiwems92xc.png" alt="Athena SELECT result showing the 2026-07-16 data inserted from Snowflake" width="799" height="294"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The data I inserted from Snowflake showed up on the Athena side too, no problem! Even for an individually added table, reads and writes work in both directions.&lt;/p&gt;

&lt;h3&gt;
  
  
  Catalog-Linked Database
&lt;/h3&gt;

&lt;p&gt;Next, let's write to &lt;code&gt;daily_sales2&lt;/code&gt;, the table that was auto detected through the Catalog-Linked Database.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;INSERT&lt;/span&gt; &lt;span class="k"&gt;INTO&lt;/span&gt; &lt;span class="n"&gt;LINKED_DB&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;analytics&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;daily_sales2&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;sale_date&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;product_category&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;sales_amount&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;VALUES&lt;/span&gt;
    &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'2024-01-01'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'Electronics'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;1500&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;00&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'2024-01-01'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'Clothing'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;800&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;50&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'2024-01-02'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'Electronics'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;2100&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;75&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'2024-01-02'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'Clothing'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;950&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;25&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'2024-01-03'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'Food'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;1200&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;00&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fl58ujyq2odjbiekpxzbj.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fl58ujyq2odjbiekpxzbj.png" alt="Inserting into daily_sales2 via CLD from Snowflake, 5 rows inserted" width="800" height="350"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Let's check on the AWS side (Athena).&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="nv"&gt;"analytics"&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nv"&gt;"daily_sales2"&lt;/span&gt; &lt;span class="k"&gt;limit&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F0mgnkum2mpm7b2rb6arg.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F0mgnkum2mpm7b2rb6arg.png" alt="Athena SELECT result on daily_sales2 showing the 5 rows inserted from Snowflake" width="800" height="291"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;This one wrote through without any issues too!&lt;/p&gt;

&lt;h1&gt;
  
  
  CREATE OR REPLACE ICEBERG TABLE vs. Catalog-Linked Database: Which to Use
&lt;/h1&gt;

&lt;p&gt;Having actually tried both, here's how I'd sum up the comparison.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Approach&lt;/th&gt;
&lt;th&gt;Table Registration&lt;/th&gt;
&lt;th&gt;Auto Detection&lt;/th&gt;
&lt;th&gt;Good Fit For&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;CATALOG INTEGRATION + CREATE ICEBERG TABLE&lt;/td&gt;
&lt;td&gt;Manual, one at a time&lt;/td&gt;
&lt;td&gt;None&lt;/td&gt;
&lt;td&gt;Validation/PoC work, or when you only want to expose a small, specific set of tables&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;CATALOG INTEGRATION + Catalog-Linked Database&lt;/td&gt;
&lt;td&gt;Automatic&lt;/td&gt;
&lt;td&gt;Yes (tables on the Glue side show up automatically)&lt;/td&gt;
&lt;td&gt;Production use where tables on the Glue/S3 Tables side keep getting added or changed&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The CREATE ICEBERG TABLE approach lets you explicitly control which tables get exposed to Snowflake, but it comes with the cost of DDL maintenance every time a table is added. CLD removes that syncing overhead, but since essentially all tables under a namespace become visible, you'll want to design your permissions more carefully if you need to control the scope of what's exposed.&lt;/p&gt;

&lt;p&gt;I confirmed this time that both approaches support bidirectional reads and writes, from Snowflake to the AWS side and from the AWS side to Snowflake.&lt;/p&gt;

&lt;h1&gt;
  
  
  What About a General Purpose Bucket?
&lt;/h1&gt;

&lt;p&gt;Everything up to this point assumed S3 Tables (a table bucket). Reading and writing an Iceberg table on a general purpose S3 bucket from Snowflake comes with a slightly different set of constraints.&lt;/p&gt;

&lt;p&gt;Combining this with the External Volume setup I touched on previously, here's how things currently stand.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Configuration&lt;/th&gt;
&lt;th&gt;Read&lt;/th&gt;
&lt;th&gt;Write&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;CATALOG INTEGRATION (&lt;code&gt;CATALOG_SOURCE = GLUE&lt;/code&gt;) + External Volume&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;CATALOG INTEGRATION (&lt;code&gt;CATALOG_SOURCE = ICEBERG_REST&lt;/code&gt;) + External Volume&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;CATALOG INTEGRATION (&lt;code&gt;CATALOG_SOURCE = ICEBERG_REST&lt;/code&gt;) + Vended Credentials&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;CATALOG INTEGRATION + Catalog-Linked Database (REST)&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Only the combination of &lt;code&gt;CATALOG_SOURCE = GLUE&lt;/code&gt; (the legacy approach that uses the Glue Data Catalog API directly, rather than going through Iceberg REST) with External Volume ends up read only. Snowflake's own documentation explicitly states that tables created this way are treated as read only, as an "Externally Managed Iceberg Table." If you want to be able to write, it looks like choosing &lt;code&gt;CATALOG_SOURCE = ICEBERG_REST&lt;/code&gt; is a prerequisite.&lt;/p&gt;

&lt;p&gt;The S3 Tables configuration I tested this time (&lt;code&gt;CATALOG_SOURCE = ICEBERG_REST&lt;/code&gt; with Vended Credentials) falls into the row that supports both reads and writes. That said, actually getting writes working required all three layers of permissions I've laid out here (IAM, Lake Formation, and the S3 Tables bucket policy) to be in place, and that was the real takeaway from this round of testing.&lt;/p&gt;

&lt;h1&gt;
  
  
  Conclusion
&lt;/h1&gt;

&lt;p&gt;In this article, I actually built out reading and writing an Iceberg table on S3 Tables from Snowflake's Catalog Integration, and worked through the permissions issues I ran into along the way.&lt;/p&gt;

&lt;p&gt;To summarize:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;To access S3 Tables from Snowflake through the Glue Iceberg REST endpoint, you need all of the following in place: the IAM policy, the trust policy, the Lake Formation data location registration (&lt;code&gt;register-resource&lt;/code&gt; with &lt;code&gt;--with-federation --with-privileged-access&lt;/code&gt; and a dedicated data access role), the Lake Formation grants across the three tiers (Catalog, Database, and Table), and the S3 Tables table bucket policy.&lt;/li&gt;
&lt;li&gt;The data access role's trust policy needs to allow not just &lt;code&gt;sts:AssumeRole&lt;/code&gt; but also &lt;code&gt;sts:SetSourceIdentity&lt;/code&gt; and &lt;code&gt;sts:SetContext&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;S3 Tables location registration can't be done from the console, and requires command line operations.&lt;/li&gt;
&lt;li&gt;CREATE ICEBERG TABLE registers tables one at a time by hand, while Catalog-Linked Database auto detects at the namespace level. The underlying permission model is the same either way, the difference is purely in the registration effort.&lt;/li&gt;
&lt;li&gt;I confirmed that both approaches support bidirectional reads and writes, from AWS to Snowflake and from Snowflake to AWS.&lt;/li&gt;
&lt;li&gt;For an Iceberg table on a general purpose S3 bucket, only the &lt;code&gt;CATALOG_SOURCE = GLUE&lt;/code&gt; plus External Volume combination is read only. Getting writes to work requires &lt;code&gt;CATALOG_SOURCE = ICEBERG_REST&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Error messages around permissions, things like "Insufficient permissions" or "Access Denied," don't tell you on their own which layer, IAM, Lake Formation, or a resource policy, is actually blocking things. Untangling that meant tracing the actual events in CloudTrail one by one.&lt;/p&gt;

&lt;p&gt;Lake Formation is also an important service for maintaining governance when integrating with Snowflake. It's a bit of a quirky service the first time you work with it, so I'd recommend reading through the documentation before diving in.&lt;/p&gt;

&lt;p&gt;Note: I work with it fairly often myself, and I still trip up on it every single time...&lt;/p&gt;

&lt;p&gt;I hope this article is useful to anyone considering connecting S3 Tables and Snowflake.&lt;/p&gt;

</description>
      <category>aws</category>
      <category>snowflake</category>
      <category>dataengineering</category>
    </item>
    <item>
      <title>Verifying How IAM and Lake Formation Behave for the Glue REST Catalog and S3 Tables</title>
      <dc:creator>Aki</dc:creator>
      <pubDate>Mon, 13 Jul 2026 22:07:59 +0000</pubDate>
      <link>https://dev.to/aws-builders/verifying-how-iam-and-lake-formation-behave-for-the-glue-rest-catalog-and-s3-tables-2cgc</link>
      <guid>https://dev.to/aws-builders/verifying-how-iam-and-lake-formation-behave-for-the-glue-rest-catalog-and-s3-tables-2cgc</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Original Japanese article&lt;/strong&gt;: &lt;a href="https://zenn.dev/penginpenguin/articles/11355eaf43c6c9" rel="noopener noreferrer"&gt;Glue REST CatalogとS3TablesのIAMとLake Formationの挙動を確かめる&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h1&gt;
  
  
  Introduction
&lt;/h1&gt;

&lt;p&gt;I'm Aki, an AWS Community Builder (&lt;a href="https://x.com/jitepengin" rel="noopener noreferrer"&gt;@jitepengin&lt;/a&gt;).&lt;/p&gt;

&lt;p&gt;In my previous article, I hit the Iceberg REST Catalog directly and confirmed the design differences between the Glue endpoint and the S3 Tables endpoint.&lt;/p&gt;

&lt;p&gt;In that article I wrote that "accessing through the Glue endpoint requires a Lake Formation grant." This time, I'll deliberately vary the IAM policy and the Lake Formation grant to see exactly how the behavior of each endpoint changes.&lt;/p&gt;

&lt;p&gt;Along the way, I'll also check whether &lt;code&gt;GetDataAccess&lt;/code&gt; is actually being called via CloudTrail, to peek inside the authorization flow itself.&lt;/p&gt;

&lt;p&gt;I previously wrote about the design differences between the Glue and S3 Tables Iceberg REST endpoints in another article, worth a read alongside this one: &lt;a href="https://dev.to/aws-builders/hitting-the-iceberg-rest-catalog-directly-understanding-the-differences-between-glue-data-catalog-43j8"&gt;Hitting the Iceberg REST Catalog Directly: Understanding the Differences Between Glue Data Catalog and S3 Tables&lt;/a&gt;&lt;/p&gt;

&lt;h1&gt;
  
  
  What We're Verifying Today
&lt;/h1&gt;

&lt;p&gt;According to AWS documentation, the Glue REST Catalog is authorized through a combination of IAM policy and Lake Formation grants, while the S3 Tables REST endpoint is authorized through IAM alone.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Glue endpoint: IAM authorization → Lake Formation authorization&lt;/li&gt;
&lt;li&gt;S3 Tables endpoint: s3tables IAM authorization only&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Let's verify how this actually plays out, using the following matrix:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Condition&lt;/th&gt;
&lt;th&gt;Glue endpoint&lt;/th&gt;
&lt;th&gt;S3 Tables endpoint&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;IAM ✓ / LF ✓&lt;/td&gt;
&lt;td&gt;200 (baseline)&lt;/td&gt;
&lt;td&gt;200 (baseline)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;IAM ✓ / LF ✗&lt;/td&gt;
&lt;td&gt;?&lt;/td&gt;
&lt;td&gt;?&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;IAM ✗ / LF ✓&lt;/td&gt;
&lt;td&gt;?&lt;/td&gt;
&lt;td&gt;?&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The IAM ✓ / LF ✗ cell is especially important:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;If only the Glue endpoint returns 403 → Glue is genuinely enforcing Lake Formation authorization&lt;/li&gt;
&lt;li&gt;If only the S3 Tables endpoint returns 200 → that confirms the difference in authorization flow&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;After each cell, I'll also check whether &lt;code&gt;lakeformation:GetDataAccess&lt;/code&gt; was called via CloudTrail, to visualize the internal authorization flow.&lt;/p&gt;

&lt;h1&gt;
  
  
  Test Environment Setup
&lt;/h1&gt;

&lt;p&gt;I'm reusing the resources created in the previous article.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Table bucket: &lt;code&gt;penguin-rest-test&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Namespace: &lt;code&gt;analytics&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Table: &lt;code&gt;daily_sales&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Glue integration: enabled&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;register-resource --with-federation&lt;/code&gt;: already configured (vended credentials are issued)&lt;/li&gt;
&lt;/ul&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Note&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;In this test, when using Glue REST Catalog's federation, the Lake Formation permission-evaluation target was the IAM role used to vend credentials. This can be confirmed from the CloudTrail &lt;code&gt;GetDataAccess&lt;/code&gt; event, where the session that assumed that IAM role is recorded as the &lt;code&gt;userIdentity&lt;/code&gt;.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;strong&gt;"IAM ✓" state&lt;/strong&gt;: the test IAM user has the following policy attached.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"Statement"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"Effect"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Allow"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"Action"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"glue:GetCatalog"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"glue:GetDatabase"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"glue:GetTable"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"Resource"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"*"&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"Effect"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Allow"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"Action"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="s2"&gt;"s3tables:GetTableBucket"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"s3tables:GetNamespace"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"s3tables:GetTable"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="s2"&gt;"s3tables:GetTableData"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"s3tables:GetTableMetadataLocation"&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"Resource"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="s2"&gt;"arn:aws:s3tables:ap-northeast-1:123456789012:bucket/penguin-rest-test"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="s2"&gt;"arn:aws:s3tables:ap-northeast-1:123456789012:bucket/penguin-rest-test/table/*"&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"Effect"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Allow"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"Action"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"lakeformation:GetDataAccess"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"Resource"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"*"&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;"LF ✓" state&lt;/strong&gt;: the IAM role used to vend credentials (&lt;code&gt;penguin-irc-test-role&lt;/code&gt;) has the following Lake Formation grants.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Database (&lt;code&gt;analytics&lt;/code&gt;): &lt;code&gt;DESCRIBE&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Table (&lt;code&gt;daily_sales&lt;/code&gt;): &lt;code&gt;SELECT&lt;/code&gt;, &lt;code&gt;DESCRIBE&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;h1&gt;
  
  
  IAM ✓ / LF ✓ (Baseline State)
&lt;/h1&gt;

&lt;h2&gt;
  
  
  Glue Endpoint
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;awscurl &lt;span class="nt"&gt;--service&lt;/span&gt; glue &lt;span class="nt"&gt;--region&lt;/span&gt; ap-northeast-1 &lt;span class="nt"&gt;--profile&lt;/span&gt; penguin-irc-test &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="s2"&gt;"https://glue.ap-northeast-1.amazonaws.com/iceberg/v1/catalogs/123456789012:s3tablescatalog:penguin-rest-test/namespaces/analytics/tables/daily_sales"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Result (excerpt):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"config"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"s3.access-key-id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"ASIAXXXXXXXXXXXXXXXX"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"s3.secret-access-key"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"(masked)"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"s3.session-token"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"(masked)"&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"metadata-location"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"s3://...(omitted)"&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;HTTP 200, with a response containing vended credentials.&lt;/p&gt;

&lt;h2&gt;
  
  
  S3 Tables Endpoint
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;BUCKET_ARN_PATH&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"arn:aws:s3tables:ap-northeast-1:123456789012:bucket%2Fpenguin-rest-test"&lt;/span&gt;

awscurl &lt;span class="nt"&gt;--service&lt;/span&gt; s3tables &lt;span class="nt"&gt;--region&lt;/span&gt; ap-northeast-1 &lt;span class="nt"&gt;--profile&lt;/span&gt; penguin-irc-test &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="s2"&gt;"https://s3tables.ap-northeast-1.amazonaws.com/iceberg/v1/&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;BUCKET_ARN_PATH&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;/namespaces/analytics/tables/daily_sales"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Result (excerpt):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"config"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"tableBucketId"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"3b2a6702-dd99-44f2-bc03-2f9f6b273104"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"namespaceId"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"20576faa-861e-499f-a30f-4d6c7550dd55"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"tableId"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"34c72c19-610d-4e5c-bee4-6ae9f093c583"&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"metadata-location"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"s3://...(omitted)"&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;HTTP 200, with a response containing internal IDs (no credentials).&lt;/p&gt;

&lt;h2&gt;
  
  
  Checking CloudTrail (Glue Endpoint → GetDataAccess)
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;aws cloudtrail lookup-events &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--lookup-attributes&lt;/span&gt; &lt;span class="nv"&gt;AttributeKey&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;EventName,AttributeValue&lt;span class="o"&gt;=&lt;/span&gt;GetDataAccess &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--region&lt;/span&gt; ap-northeast-1 &lt;span class="nt"&gt;--max-results&lt;/span&gt; 3
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Result (excerpt):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"EventName"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"GetDataAccess"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"EventTime"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"2026-07-10T14:39:35+09:00"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"EventSource"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"lakeformation.amazonaws.com"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"Username"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"penguin-irc-test-session"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"CloudTrailEvent"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"...&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;invokedBy&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;:&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;glue.amazonaws.com&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;..."&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;GetDataAccess&lt;/code&gt; is recorded with &lt;code&gt;invokedBy: glue.amazonaws.com&lt;/code&gt;. The event was logged within seconds of when the request was sent, which shows that Glue queries Lake Formation synchronously.&lt;/p&gt;

&lt;p&gt;No &lt;code&gt;GetDataAccess&lt;/code&gt; was recorded for the request made against the S3 Tables endpoint.&lt;/p&gt;

&lt;h2&gt;
  
  
  Checking CloudTrail (the S3 Tables Endpoint's Own Call)
&lt;/h2&gt;

&lt;p&gt;In addition to confirming that &lt;code&gt;GetDataAccess&lt;/code&gt; isn't recorded, let's also check how the S3 Tables endpoint's own API call (&lt;code&gt;GetTableMetadataLocation&lt;/code&gt;) shows up in CloudTrail.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;aws cloudtrail lookup-events &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--lookup-attributes&lt;/span&gt; &lt;span class="nv"&gt;AttributeKey&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;EventName,AttributeValue&lt;span class="o"&gt;=&lt;/span&gt;GetTableMetadataLocation &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--region&lt;/span&gt; ap-northeast-1 &lt;span class="nt"&gt;--max-results&lt;/span&gt; 3 &lt;span class="nt"&gt;--profile&lt;/span&gt; penguin-irc-test
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Result (excerpt, masked):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"EventName"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"GetTableMetadataLocation"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"EventTime"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"2026-07-10T14:41:05+09:00"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"EventSource"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"s3tables.amazonaws.com"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"Username"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"penguin-irc-test-session"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"CloudTrailEvent"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"{&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;userIdentity&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;:{&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;type&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;:&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;AssumedRole&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;,&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;arn&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;:&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;arn:aws:sts::123456789012:assumed-role/penguin-irc-test-role/penguin-irc-test-session&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;},&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;eventSource&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;:&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;s3tables.amazonaws.com&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;,&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;eventName&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;:&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;GetTableMetadataLocation&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;,...}"&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;eventSource&lt;/code&gt; is recorded simply as &lt;code&gt;s3tables.amazonaws.com&lt;/code&gt; on its own, and there's no field equivalent to the &lt;code&gt;invokedBy&lt;/code&gt; we saw in the Glue-side &lt;code&gt;GetDataAccess&lt;/code&gt; event anywhere in the CloudTrailEvent. This confirms, from this angle as well, that authorization on the S3 Tables endpoint is a direct IAM evaluation of the caller's own credentials, rather than a delegated call through another service.&lt;/p&gt;

&lt;p&gt;Whether &lt;code&gt;Username&lt;/code&gt; shows up as &lt;code&gt;penguin-irc-test&lt;/code&gt; (the IAM user itself) or &lt;code&gt;penguin-irc-test-session&lt;/code&gt; (the session for the role used to vend credentials) lines up exactly with &lt;code&gt;userIdentity.type&lt;/code&gt; (&lt;code&gt;IAMUser&lt;/code&gt; vs. &lt;code&gt;AssumedRole&lt;/code&gt;), giving us a way to tell which principal actually processed each request.&lt;/p&gt;

&lt;h1&gt;
  
  
  IAM ✓ / LF ✗ (Removing the LF Grant)
&lt;/h1&gt;

&lt;p&gt;Let's remove the Lake Formation grant from the IAM role used to vend credentials (&lt;code&gt;penguin-irc-test-role&lt;/code&gt;).&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;aws lakeformation revoke-permissions &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--principal&lt;/span&gt; &lt;span class="nv"&gt;DataLakePrincipalIdentifier&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;arn:aws:iam::123456789012:role/penguin-irc-test-role &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--permissions&lt;/span&gt; &lt;span class="s2"&gt;"SELECT"&lt;/span&gt; &lt;span class="s2"&gt;"DESCRIBE"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--resource&lt;/span&gt; &lt;span class="s1"&gt;'{"Table":{"CatalogId":"123456789012:s3tablescatalog/penguin-rest-test","DatabaseName":"analytics","Name":"daily_sales"}}'&lt;/span&gt;

aws lakeformation revoke-permissions &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--principal&lt;/span&gt; &lt;span class="nv"&gt;DataLakePrincipalIdentifier&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;arn:aws:iam::123456789012:role/penguin-irc-test-role &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--permissions&lt;/span&gt; &lt;span class="s2"&gt;"DESCRIBE"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--resource&lt;/span&gt; &lt;span class="s1"&gt;'{"Database":{"CatalogId":"123456789012:s3tablescatalog/penguin-rest-test","Name":"analytics"}}'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Glue Endpoint
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"error"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"code"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;403&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"message"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Insufficient Lake Formation permission(s): Required Describe on daily_sales"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"AccessDeniedException"&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;HTTP 403, with an error indicating insufficient Lake Formation permissions.&lt;/p&gt;

&lt;h2&gt;
  
  
  S3 Tables Endpoint
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"config"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"tableBucketId"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"3b2a6702-dd99-44f2-bc03-2f9f6b273104"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"namespaceId"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"20576faa-861e-499f-a30f-4d6c7550dd55"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"tableId"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"34c72c19-610d-4e5c-bee4-6ae9f093c583"&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"metadata-location"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"s3://...(omitted)"&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Still 200, no change. Whether or not the Lake Formation grant exists has no effect on authorization for the S3 Tables endpoint.&lt;/p&gt;

&lt;h2&gt;
  
  
  Checking CloudTrail (Glue Endpoint → GetDataAccess)
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;aws cloudtrail lookup-events &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--lookup-attributes&lt;/span&gt; &lt;span class="nv"&gt;AttributeKey&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;EventName,AttributeValue&lt;span class="o"&gt;=&lt;/span&gt;GetDataAccess &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--region&lt;/span&gt; ap-northeast-1 &lt;span class="nt"&gt;--max-results&lt;/span&gt; 3
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Result (excerpt):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"EventName"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"GetDataAccess"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"EventTime"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"2026-07-10T14:51:29+09:00"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"EventSource"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"lakeformation.amazonaws.com"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"Username"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"penguin-irc-test-session"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"CloudTrailEvent"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"...&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;invokedBy&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;:&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;glue.amazonaws.com&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;..."&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Even after removing the Lake Formation grant, &lt;code&gt;GetDataAccess&lt;/code&gt; is recorded with &lt;code&gt;invokedBy: glue.amazonaws.com&lt;/code&gt;, at roughly the same time the request was sent. For the table-retrieval operation tested here on the Glue endpoint, &lt;code&gt;GetDataAccess&lt;/code&gt; is recorded even in the 403 case, confirming that the request does reach Lake Formation's permission evaluation.&lt;/p&gt;

&lt;h1&gt;
  
  
  IAM ✗ / LF ✓ (Removing the s3tables IAM Actions)
&lt;/h1&gt;

&lt;p&gt;After restoring the Lake Formation grant on the role, let's remove the s3tables-related actions from the IAM policy.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="err"&gt;aws&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;iam&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;put-user-policy&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;\&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="err"&gt;--user-name&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;penguin-irc-test&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;\&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="err"&gt;--policy-name&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;GlueIRCMinimal&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;\&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="err"&gt;--policy-document&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;'&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"Version"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"2012-10-17"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"Statement"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"Effect"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Allow"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"Action"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"glue:GetCatalog"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="s2"&gt;"glue:GetDatabase"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="s2"&gt;"glue:GetTable"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"Resource"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"*"&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"Effect"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Allow"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"Action"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"lakeformation:GetDataAccess"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"Resource"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"*"&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="err"&gt;'&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Glue Endpoint
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"error"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"code"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;403&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"message"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"From federation source: User: arn:aws:iam::123456789012:user/penguin-irc-test is not authorized to perform: s3tables:GetTableBucket on resource: arn:aws:s3tables:ap-northeast-1:123456789012:bucket/penguin-rest-test because no identity-based policy allows the s3tables:GetTableBucket action"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"AccessDeniedException"&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;HTTP 403. In this test, when the calling IAM user lacked s3tables permissions, we got a &lt;code&gt;From federation source&lt;/code&gt; error, confirming that the request never even reaches Lake Formation's evaluation.&lt;/p&gt;

&lt;h2&gt;
  
  
  S3 Tables Endpoint
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"error"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"code"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;403&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"message"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"User: arn:aws:iam::123456789012:user/penguin-irc-test is not authorized to perform: s3tables:GetTableMetadataLocation on resource: ..."&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"forbidden"&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Also 403, unsurprising, since the s3tables IAM actions are missing.&lt;/p&gt;

&lt;h2&gt;
  
  
  Checking CloudTrail (Glue Endpoint → GetDataAccess)
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;aws cloudtrail lookup-events &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--lookup-attributes&lt;/span&gt; &lt;span class="nv"&gt;AttributeKey&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;EventName,AttributeValue&lt;span class="o"&gt;=&lt;/span&gt;GetDataAccess &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--region&lt;/span&gt; ap-northeast-1 &lt;span class="nt"&gt;--max-results&lt;/span&gt; 3
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Result (excerpt):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;No matching GetDataAccess event was found.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;No &lt;code&gt;GetDataAccess&lt;/code&gt; was recorded corresponding to this request. This confirms that when the s3tables-related IAM permissions are removed, Lake Formation is never even queried in the first place, and the request is denied at the IAM authorization stage.&lt;/p&gt;

&lt;h1&gt;
  
  
  Results Summary
&lt;/h1&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Condition&lt;/th&gt;
&lt;th&gt;Glue endpoint&lt;/th&gt;
&lt;th&gt;S3 Tables endpoint&lt;/th&gt;
&lt;th&gt;GetDataAccess&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;IAM ✓ / LF ✓&lt;/td&gt;
&lt;td&gt;200&lt;/td&gt;
&lt;td&gt;200&lt;/td&gt;
&lt;td&gt;Present (14:39:35, recorded under the federation role's session)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;IAM ✓ / LF ✗&lt;/td&gt;
&lt;td&gt;403&lt;/td&gt;
&lt;td&gt;200&lt;/td&gt;
&lt;td&gt;Present (14:51:29, recorded under the federation role's session)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;IAM ✗ / LF ✓&lt;/td&gt;
&lt;td&gt;403&lt;/td&gt;
&lt;td&gt;403&lt;/td&gt;
&lt;td&gt;Absent&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h1&gt;
  
  
  Authorization Flow
&lt;/h1&gt;

&lt;p&gt;The results confirm that each endpoint follows a genuinely different flow.&lt;/p&gt;

&lt;h2&gt;
  
  
  Glue REST Catalog
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Calling IAM user
    |
    | Checks s3tables IAM permissions
    | via the Glue API
    ↓
Glue REST Catalog
    |
    | GetDataAccess
    | (evaluated as the federation IAM role)
    ↓
Lake Formation grant evaluation
    |
    ↓
Vended credentials issued
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Here's what this test confirmed:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;In this test, when the calling IAM user lacked s3tables IAM permissions, the request was denied at the IAM authorization stage without ever reaching Lake Formation's evaluation (this is where the &lt;code&gt;From federation source&lt;/code&gt; error appears).&lt;/li&gt;
&lt;li&gt;Once the s3tables IAM permissions are satisfied, Lake Formation permission evaluation is performed with the IAM role used to vend credentials as the principal.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;GetDataAccess&lt;/code&gt; is recorded in CloudTrail as &lt;code&gt;invokedBy: glue.amazonaws.com&lt;/code&gt; (it's recorded even in the 403 case when LF permissions are missing). The request time and the event time are nearly identical, showing that the evaluation happens synchronously.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  S3 Tables REST Endpoint
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Calling IAM user
    |
    | s3tables IAM authorization
    ↓
S3 Tables REST endpoint
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This test confirmed that the S3 Tables REST endpoint operates purely on an IAM-based authorization model.&lt;/p&gt;

&lt;p&gt;Here's what this test confirmed:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Access is controlled by the calling user's IAM policy for s3tables API authorization (no Lake Formation involvement was observed in this test).&lt;/li&gt;
&lt;li&gt;Lake Formation-based permission control was not observed on this S3 Tables REST endpoint access path.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;GetDataAccess&lt;/code&gt; is not recorded in CloudTrail.&lt;/li&gt;
&lt;/ol&gt;

&lt;h1&gt;
  
  
  Conclusion
&lt;/h1&gt;

&lt;p&gt;In this article, I confirmed the authorization flow for the Glue REST Catalog and the S3 Tables REST endpoint by switching the IAM policy and Lake Formation grants on and off.&lt;/p&gt;

&lt;p&gt;To summarize:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;For the Glue REST Catalog, Lake Formation's &lt;code&gt;GetDataAccess&lt;/code&gt; is executed only after the calling IAM user's s3tables-related IAM permissions are satisfied.&lt;/li&gt;
&lt;li&gt;Lake Formation's permission evaluation is performed with the IAM role used to vend credentials as the principal, not the calling IAM user.&lt;/li&gt;
&lt;li&gt;Even when a Lake Formation permission shortfall results in a 403, &lt;code&gt;GetDataAccess&lt;/code&gt; is still recorded in CloudTrail, confirming that the request does reach Lake Formation's permission evaluation (the request time and event time are nearly identical).&lt;/li&gt;
&lt;li&gt;On the other hand, when the calling IAM user's s3tables-related IAM permissions are insufficient, &lt;code&gt;GetDataAccess&lt;/code&gt; is not recorded, and Lake Formation's permission evaluation never runs.&lt;/li&gt;
&lt;li&gt;For the S3 Tables REST endpoint, within the scope of this test, &lt;code&gt;GetDataAccess&lt;/code&gt; was never recorded and no Lake Formation involvement was observed. The S3 Tables endpoint's own CloudTrail events also have no field equivalent to &lt;code&gt;invokedBy&lt;/code&gt;, further confirming that authorization is a direct IAM evaluation.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;By combining CloudTrail with the actual API responses, I was able to observe the real authorization flow, something that isn't visible from the documentation's description of "Lake Formation is used" alone.&lt;/p&gt;

&lt;p&gt;The CloudTrail logs also back up the difference that the Glue REST Catalog goes through both IAM and Lake Formation authorization, while the S3 Tables REST endpoint is authorized purely through IAM.&lt;/p&gt;

&lt;p&gt;To be fair, this all matched the documented behavior, but going through it hands-on still surfaced plenty of small insights and things I learned along the way.&lt;/p&gt;

&lt;p&gt;I've found that combining CloudTrail with API responses like this is a genuinely useful technique for understanding the internal behavior of other AWS services too. I'd like to keep running experiments like this to uncover things about AWS's internals that documentation alone doesn't show.&lt;/p&gt;

&lt;p&gt;I hope this article is useful to anyone trying to understand the authorization architecture behind the Iceberg REST Catalog on AWS.&lt;/p&gt;

</description>
      <category>aws</category>
      <category>dataengineering</category>
    </item>
    <item>
      <title>Hitting the Iceberg REST Catalog Directly: Understanding the Differences Between Glue Data Catalog and S3 Tables</title>
      <dc:creator>Aki</dc:creator>
      <pubDate>Fri, 10 Jul 2026 03:32:10 +0000</pubDate>
      <link>https://dev.to/aws-builders/hitting-the-iceberg-rest-catalog-directly-understanding-the-differences-between-glue-data-catalog-43j8</link>
      <guid>https://dev.to/aws-builders/hitting-the-iceberg-rest-catalog-directly-understanding-the-differences-between-glue-data-catalog-43j8</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Original Japanese article&lt;/strong&gt;: &lt;a href="https://zenn.dev/penginpenguin/articles/9b43fececff5b2" rel="noopener noreferrer"&gt;Iceberg REST Catalogを直接叩いて、Glue Data CatalogとS3 Tablesの違いを理解する&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h1&gt;
  
  
  Introduction
&lt;/h1&gt;

&lt;p&gt;I'm Aki, an AWS Community Builder (&lt;a href="https://x.com/jitepengin" rel="noopener noreferrer"&gt;@jitepengin&lt;/a&gt;).&lt;/p&gt;

&lt;p&gt;Most of the time, when working with Iceberg tables, we reach for PyIceberg or Spark. I'm no exception, and honestly there were parts of the PyIceberg configuration — &lt;code&gt;rest.sigv4-enabled&lt;/code&gt;, &lt;code&gt;rest.signing-name&lt;/code&gt;, &lt;code&gt;warehouse&lt;/code&gt; — that I understood only vaguely.&lt;/p&gt;

&lt;p&gt;Iceberg defines a standard called the &lt;a href="https://github.com/apache/iceberg/blob/main/open-api/rest-catalog-open-api.yaml" rel="noopener noreferrer"&gt;Iceberg REST Catalog Open API specification&lt;/a&gt;, and AWS implements it through two separate endpoints:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The AWS Glue Iceberg REST endpoint (&lt;code&gt;https://glue.&amp;lt;region&amp;gt;.amazonaws.com/iceberg&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;The Amazon S3 Tables Iceberg REST endpoint (&lt;code&gt;https://s3tables.&amp;lt;region&amp;gt;.amazonaws.com/iceberg&lt;/code&gt;)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If two implementations follow the same spec, sending the same requests to both and comparing the results should reveal what's actually different between them.&lt;/p&gt;

&lt;p&gt;In this article, I'll bypass clients like PyIceberg entirely and hit the REST API directly to explore the differences between the two endpoints.&lt;/p&gt;

&lt;p&gt;To state the conclusion up front:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Even though both implement the same Iceberg REST Catalog specification, Glue is designed as an "entry point to multiple catalogs," while S3 Tables is designed as an "entry point to a single table bucket." That difference is visible just by looking at the URL paths.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;I previously wrote about the relationship between S3 Tables and Glue Data Catalog in another article — worth a read alongside this one:&lt;br&gt;
&lt;a href="https://dev.to/aws-builders/does-amazon-s3-tables-replace-aws-glue-data-catalog-understanding-their-relationship-104a"&gt;Does Amazon S3 Tables Replace AWS Glue Data Catalog? Understanding Their Relationship&lt;/a&gt;&lt;/p&gt;


&lt;h1&gt;
  
  
  What Is the Iceberg REST Catalog?
&lt;/h1&gt;

&lt;p&gt;The Iceberg REST Catalog is a specification that standardizes Iceberg catalog operations as an HTTP API. It's published as an OpenAPI definition (YAML), and any catalog that conforms to it can be accessed the same way from clients such as PyIceberg, Spark, and Trino.&lt;/p&gt;

&lt;p&gt;The key points of the spec are:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;URL paths follow a pattern like &lt;code&gt;GET /v1/{prefix}/namespaces&lt;/code&gt;, where &lt;code&gt;{prefix}&lt;/code&gt; is a free-form segment&lt;/li&gt;
&lt;li&gt;Clients first call &lt;code&gt;GET /v1/config&lt;/code&gt; to retrieve endpoint configuration (the default &lt;code&gt;prefix&lt;/code&gt; and other settings)&lt;/li&gt;
&lt;li&gt;The table metadata body (schema, snapshots, etc.) is returned as JSON in the &lt;code&gt;LoadTable&lt;/code&gt; response&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;In other words, when you write &lt;code&gt;catalog.load_table("ns.table")&lt;/code&gt; in PyIceberg, what's actually happening under the hood is an HTTP request to &lt;code&gt;GET /v1/{prefix}/namespaces/ns/tables/table&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Here's a summary of the two AWS implementations before we dive in:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Item&lt;/th&gt;
&lt;th&gt;Glue Iceberg REST endpoint&lt;/th&gt;
&lt;th&gt;S3 Tables Iceberg REST endpoint&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Endpoint&lt;/td&gt;
&lt;td&gt;&lt;code&gt;https://glue.&amp;lt;region&amp;gt;.amazonaws.com/iceberg&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;https://s3tables.&amp;lt;region&amp;gt;.amazonaws.com/iceberg&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Contents of &lt;code&gt;{prefix}&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;/catalogs/{catalog}&lt;/code&gt; (catalog hierarchy)&lt;/td&gt;
&lt;td&gt;URL-encoded table bucket ARN&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Value passed as &lt;code&gt;warehouse&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Glue catalog ID&lt;/td&gt;
&lt;td&gt;Table bucket ARN&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;SigV4 signing service name&lt;/td&gt;
&lt;td&gt;&lt;code&gt;glue&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;s3tables&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Access control&lt;/td&gt;
&lt;td&gt;IAM + Lake Formation&lt;/td&gt;
&lt;td&gt;s3tables IAM actions only&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Here's the overall picture as a diagram:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;              Iceberg REST Catalog spec
           GET /v1/{prefix}/namespaces/...
                        │
        ┌───────────────┴───────────────┐
        │                               │
  Glue endpoint                  S3 Tables endpoint
        │                               │
 prefix = /catalogs/{catalog}    prefix = table bucket ARN
 (multi-catalog hierarchy)       (one bucket = one catalog)
        │                               │
  IAM + Lake Formation           s3tables IAM actions
        │                               │
        └───────────────┬───────────────┘
                        │
              Same Iceberg table
          (points to the same metadata.json)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;blockquote&gt;
&lt;p&gt;Note: a catalog doesn't manage the &lt;code&gt;metadata.json&lt;/code&gt; file itself — it provides a reference to where the latest &lt;code&gt;metadata-location&lt;/code&gt; is. The catalog's essential job is knowing where the current &lt;code&gt;metadata.json&lt;/code&gt; lives.&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h1&gt;
  
  
  Setting Up the Test Environment
&lt;/h1&gt;

&lt;p&gt;Let's create a test table bucket, namespace, and table via the CLI. Read the region as Tokyo (&lt;code&gt;ap-northeast-1&lt;/code&gt;) and the account ID as &lt;code&gt;123456789012&lt;/code&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Create a table bucket&lt;/span&gt;
aws s3tables create-table-bucket &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--name&lt;/span&gt; penguin-rest-test &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--region&lt;/span&gt; ap-northeast-1

&lt;span class="c"&gt;# Create a namespace&lt;/span&gt;
aws s3tables create-namespace &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--table-bucket-arn&lt;/span&gt; arn:aws:s3tables:ap-northeast-1:123456789012:bucket/penguin-rest-test &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--namespace&lt;/span&gt; analytics

&lt;span class="c"&gt;# Create a table (with schema)&lt;/span&gt;
aws s3tables create-table &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--table-bucket-arn&lt;/span&gt; arn:aws:s3tables:ap-northeast-1:123456789012:bucket/penguin-rest-test &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--namespace&lt;/span&gt; analytics &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--name&lt;/span&gt; daily_sales &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--format&lt;/span&gt; ICEBERG &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--metadata&lt;/span&gt; &lt;span class="s1"&gt;'{
    "iceberg": {
      "schema": {
        "fields": [
          {"name": "sales_date", "type": "date", "required": false},
          {"name": "amount", "type": "long", "required": false}
        ]
      }
    }
  }'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h1&gt;
  
  
  SigV4 Signing
&lt;/h1&gt;

&lt;p&gt;You can't hit these endpoints with plain &lt;code&gt;curl&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The Iceberg REST Catalog spec defines an OAuth2-based authentication flow, but AWS's implementation uses IAM SigV4 signing instead — a standard-spec API with AWS-flavored authentication.&lt;/p&gt;

&lt;p&gt;That &lt;code&gt;rest.sigv4-enabled: true&lt;/code&gt; setting in PyIceberg is exactly what enables this signing.&lt;/p&gt;

&lt;p&gt;Computing SigV4 signatures by hand is painful, so I used &lt;a href="https://github.com/okigan/awscurl" rel="noopener noreferrer"&gt;awscurl&lt;/a&gt; for this exercise.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;pip &lt;span class="nb"&gt;install &lt;/span&gt;awscurl
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;awscurl&lt;/code&gt; picks up credentials from environment variables or a profile and sends SigV4-signed requests for you.&lt;/p&gt;

&lt;p&gt;The important part is the &lt;code&gt;--service&lt;/code&gt; option, which must specify the correct service name for signing: &lt;code&gt;glue&lt;/code&gt; for the Glue endpoint, &lt;code&gt;s3tables&lt;/code&gt; for the S3 Tables endpoint.&lt;/p&gt;




&lt;h1&gt;
  
  
  Hitting the S3 Tables Iceberg REST Endpoint
&lt;/h1&gt;

&lt;p&gt;Let's start with the simpler of the two — the S3 Tables endpoint.&lt;/p&gt;

&lt;h3&gt;
  
  
  GET /v1/config
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;getConfig&lt;/code&gt; is the first API a REST Catalog client calls. The &lt;code&gt;warehouse&lt;/code&gt; query parameter takes the table bucket ARN.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Note&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;When using &lt;code&gt;awscurl&lt;/code&gt;, pass the &lt;strong&gt;raw, unencoded ARN&lt;/strong&gt; as the query parameter value. &lt;code&gt;awscurl&lt;/code&gt; automatically URL-encodes query parameters before computing the signature, so if you pre-encode the value yourself, it gets double-encoded and you'll get a &lt;code&gt;SignatureDoesNotMatch&lt;/code&gt; error.&lt;br&gt;
&lt;/p&gt;
&lt;/blockquote&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;BUCKET_ARN&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"arn:aws:s3tables:ap-northeast-1:123456789012:bucket/penguin-rest-test"&lt;/span&gt;

awscurl &lt;span class="nt"&gt;--service&lt;/span&gt; s3tables &lt;span class="nt"&gt;--region&lt;/span&gt; ap-northeast-1 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="s2"&gt;"https://s3tables.ap-northeast-1.amazonaws.com/iceberg/v1/config?warehouse=&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;BUCKET_ARN&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Result (excerpt):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"defaults"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"prefix"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"arn%3Aaws%3As3tables%3Aap-northeast-1%3A123456789012%3Abucket%2Fpenguin-rest-test"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"io-impl"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"org.apache.iceberg.aws.s3.S3FileIO"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"write.object-storage.enabled"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"true"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"write.object-storage.partitioned-paths"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"false"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"s3.delete-enabled"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"false"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"rest-metrics-reporting-enabled"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"false"&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"overrides"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The interesting part is the &lt;code&gt;prefix&lt;/code&gt; in the response. For the S3 Tables endpoint, &lt;code&gt;prefix&lt;/code&gt; is exactly the URL-encoded table bucket ARN, and it lives under &lt;code&gt;defaults&lt;/code&gt;. In other words, this endpoint is designed around a "one table bucket = one catalog" model — if you want to work with multiple table buckets, you register multiple catalogs on the client side.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;defaults&lt;/code&gt; also included the write-side &lt;code&gt;FileIO&lt;/code&gt; implementation (&lt;code&gt;io-impl&lt;/code&gt;), object storage layout settings (&lt;code&gt;write.object-storage.*&lt;/code&gt;), and a setting that disables S3 delete operations (&lt;code&gt;s3.delete-enabled&lt;/code&gt;). It's interesting to see, from actual output, that some of the parameters we're used to configuring individually on the client (PyIceberg) side are actually being pushed down as server-side defaults through this config response.&lt;/p&gt;

&lt;p&gt;Incidentally, this &lt;code&gt;getConfig&lt;/code&gt; call is authorized under the &lt;code&gt;s3tables:GetTableBucket&lt;/code&gt; IAM action. The official documentation includes a mapping table showing which s3tables IAM action corresponds to each REST operation.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Note&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The spec's &lt;code&gt;CatalogConfig&lt;/code&gt; also defines an &lt;code&gt;endpoints&lt;/code&gt; field, which lets the server return a list of supported endpoints (in a format like &lt;code&gt;"GET /v1/{prefix}/namespaces"&lt;/code&gt;). When I checked, the S3 Tables endpoint's response did not include an &lt;code&gt;endpoints&lt;/code&gt; field.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h3&gt;
  
  
  Listing Namespaces and Tables
&lt;/h3&gt;

&lt;p&gt;Now that we know the prefix, let's list namespaces and tables.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Note&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;There's a subtlety worth calling out here.&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;warehouse&lt;/code&gt; query parameter for &lt;code&gt;GET /v1/config&lt;/code&gt; needs the raw ARN, since &lt;code&gt;awscurl&lt;/code&gt; automatically URL-encodes query parameters before signing — pre-encoding it yourself causes double-encoding and a &lt;code&gt;SignatureDoesNotMatch&lt;/code&gt; error.&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;{prefix}&lt;/code&gt; segment in the &lt;em&gt;path&lt;/em&gt;, however, behaves differently. If you put the ARN into the path completely unencoded (colons and slashes as-is), the &lt;code&gt;/&lt;/code&gt; characters inside the ARN get interpreted as path separators, and the request no longer matches the modeled URL pattern &lt;code&gt;/v1/{prefix}/namespaces&lt;/code&gt; — you get an &lt;code&gt;UnknownOperationException&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The correct approach is to leave the colons raw but percent-encode only the single &lt;code&gt;/&lt;/code&gt; inside &lt;code&gt;bucket/&amp;lt;table-bucket-name&amp;gt;&lt;/code&gt; as &lt;code&gt;%2F&lt;/code&gt;. That makes the request match the correct route (and once it reaches the authorization layer, you'll get a 403 if you lack permissions, rather than a routing error).&lt;/p&gt;

&lt;p&gt;Even though it's the same operation — putting an ARN into a URL — the encoding rules differ between query parameters and path segments. That's something you only really notice by actually hitting the endpoint.&lt;br&gt;
&lt;/p&gt;
&lt;/blockquote&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;BUCKET_ARN_PATH&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"arn:aws:s3tables:ap-northeast-1:123456789012:bucket%2Fpenguin-rest-test"&lt;/span&gt;

&lt;span class="c"&gt;# List namespaces&lt;/span&gt;
awscurl &lt;span class="nt"&gt;--service&lt;/span&gt; s3tables &lt;span class="nt"&gt;--region&lt;/span&gt; ap-northeast-1 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="s2"&gt;"https://s3tables.ap-northeast-1.amazonaws.com/iceberg/v1/&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;BUCKET_ARN_PATH&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;/namespaces"&lt;/span&gt;

&lt;span class="c"&gt;# List tables&lt;/span&gt;
awscurl &lt;span class="nt"&gt;--service&lt;/span&gt; s3tables &lt;span class="nt"&gt;--region&lt;/span&gt; ap-northeast-1 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="s2"&gt;"https://s3tables.ap-northeast-1.amazonaws.com/iceberg/v1/&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;BUCKET_ARN_PATH&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;/namespaces/analytics/tables"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Result (excerpt):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"namespaces"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"analytics"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"identifiers"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"daily_sales"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"namespace"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"analytics"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;There's one difference from the spec worth noting here: the Iceberg REST spec allows multi-level namespaces (like &lt;code&gt;a.b.c&lt;/code&gt;), but S3 Tables only supports a single level.&lt;/p&gt;

&lt;p&gt;Let's confirm this by trying to create a multi-level namespace:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;awscurl &lt;span class="nt"&gt;--service&lt;/span&gt; s3tables &lt;span class="nt"&gt;--region&lt;/span&gt; ap-northeast-1 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-X&lt;/span&gt; POST &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Content-Type: application/json"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="s1"&gt;'{"namespace": ["level1", "level2"]}'&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="s2"&gt;"https://s3tables.ap-northeast-1.amazonaws.com/iceberg/v1/&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;BUCKET_ARN_PATH&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;/namespaces"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Result (excerpt):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"error"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"code"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;400&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"message"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Multipart namespaces are not supported."&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"bad_request"&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The error message spells it out directly — "multipart namespaces are not supported" — confirming the single-level restriction empirically.&lt;/p&gt;

&lt;h3&gt;
  
  
  Retrieving Metadata with LoadTable
&lt;/h3&gt;

&lt;p&gt;This is the API I most wanted to check today.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;awscurl &lt;span class="nt"&gt;--service&lt;/span&gt; s3tables &lt;span class="nt"&gt;--region&lt;/span&gt; ap-northeast-1 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="s2"&gt;"https://s3tables.ap-northeast-1.amazonaws.com/iceberg/v1/&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;BUCKET_ARN_PATH&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;/namespaces/analytics/tables/daily_sales"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Result (excerpt):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"metadata-location"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"s3://34c72c19-610d-4e5c-d8d1qwx3db1p3tbr8g9jch9n6e14sapn1b--table-s3/metadata/00000-163a447a-c64d-44f9-8619-5db9561e549b.metadata.json"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"metadata"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"format-version"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"table-uuid"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"9507ea2b-7a71-40a7-b6a9-1d6e633955e1"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"location"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"s3://34c72c19-610d-4e5c-d8d1qwx3db1p3tbr8g9jch9n6e14sapn1b--table-s3"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"current-schema-id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"schemas"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"struct"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"schema-id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"fields"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"sales_date"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"required"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"date"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"amount"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"required"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"long"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"default-spec-id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"partition-specs"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"spec-id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"fields"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}],&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"properties"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"write.parquet.compression-codec"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"zstd"&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"current-snapshot-id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;-1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"snapshots"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"config"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"tableBucketId"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"3b2a6702-dd99-44f2-bc03-2f9f6b273104"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"namespaceId"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"20576faa-861e-499f-a30f-4d6c7550dd55"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"tableId"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"34c72c19-610d-4e5c-bee4-6ae9f093c583"&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This JSON is exactly what's behind the information we normally see through &lt;code&gt;table.schema()&lt;/code&gt; or &lt;code&gt;table.snapshots()&lt;/code&gt; in PyIceberg. The response directly confirms that a catalog's essential job is "managing and returning the S3 path of the latest &lt;code&gt;metadata.json&lt;/code&gt;" (&lt;code&gt;metadata-location&lt;/code&gt;).&lt;/p&gt;

&lt;p&gt;Incidentally, the &lt;code&gt;config&lt;/code&gt; field in the response contained S3 Tables-internal identifiers: &lt;code&gt;tableBucketId&lt;/code&gt;, &lt;code&gt;namespaceId&lt;/code&gt;, and &lt;code&gt;tableId&lt;/code&gt;. Per spec, the Glue endpoint's &lt;code&gt;config&lt;/code&gt; is supposed to include temporary credentials (vended credentials), but here on the S3 Tables endpoint there's no credential delegation at all — it just returns internal management IDs.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Note&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The S3 Tables endpoint also documents a limit: operations against a table whose &lt;code&gt;metadata.json&lt;/code&gt; exceeds 50MB return a 400 error. Worth keeping in mind — if metadata bloats from accumulated snapshots and similar growth, you could hit this API-level ceiling.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h3&gt;
  
  
  Checking What's Happening Under the Hood via CloudTrail
&lt;/h3&gt;

&lt;p&gt;An interesting characteristic of the S3 Tables endpoint is that REST API calls get logged in CloudTrail as their corresponding native S3 Tables actions. Per the official documentation, a single &lt;code&gt;LoadTable&lt;/code&gt; call logs both of the following:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;GetTableMetadataLocation&lt;/code&gt; (a management event)&lt;/li&gt;
&lt;li&gt;A data event corresponding to &lt;code&gt;GetTableData&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;In other words, the audit log itself reveals that this endpoint is implemented as a proxy that translates the Iceberg REST API into native S3 Tables API calls.&lt;/p&gt;

&lt;p&gt;Let's check CloudTrail directly:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;aws cloudtrail lookup-events &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--lookup-attributes&lt;/span&gt; &lt;span class="nv"&gt;AttributeKey&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;EventName,AttributeValue&lt;span class="o"&gt;=&lt;/span&gt;GetTableMetadataLocation &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--region&lt;/span&gt; ap-northeast-1 &lt;span class="se"&gt;\&lt;/span&gt;
| jq &lt;span class="s1"&gt;'.Events[0] | {time: .EventTime, user: .Username, event: .EventName}'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Result (excerpt):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"time"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"2026-07-09T13:46:44+00:00"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"user"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"penguin-test-user"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"event"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"GetTableMetadataLocation"&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Even though we only called &lt;code&gt;LoadTable&lt;/code&gt; over REST, CloudTrail logs it under the native S3 Tables event name &lt;code&gt;s3tables:GetTableMetadataLocation&lt;/code&gt;, complete with the exact caller (IAM username).&lt;/p&gt;




&lt;h1&gt;
  
  
  Hitting the Glue Iceberg REST Endpoint
&lt;/h1&gt;

&lt;p&gt;Now let's look at the Glue endpoint, which behaves quite differently.&lt;/p&gt;

&lt;h3&gt;
  
  
  GET /v1/config
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;awscurl &lt;span class="nt"&gt;--service&lt;/span&gt; glue &lt;span class="nt"&gt;--region&lt;/span&gt; ap-northeast-1 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="s2"&gt;"https://glue.ap-northeast-1.amazonaws.com/iceberg/v1/config?warehouse=123456789012"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Result (excerpt):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"defaults"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"prefix"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"123456789012"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"header.Content-Type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"application/x-amz-json-1.1"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"rest.sigv4-enabled"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"true"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"rest.signing-name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"glue"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"rest.signing-region"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"ap-northeast-1"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"rest-table-scan-enabled"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"true"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"rest-data-commit-enabled"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"true"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"token-refresh-enabled"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"false"&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"overrides"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"prefix"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"catalogs/123456789012"&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;There's a difference from the S3 Tables endpoint worth calling out here: &lt;code&gt;defaults.prefix&lt;/code&gt; is &lt;code&gt;"123456789012"&lt;/code&gt; (just the account ID), while &lt;code&gt;overrides.prefix&lt;/code&gt; is &lt;code&gt;"catalogs/123456789012"&lt;/code&gt; (with &lt;code&gt;catalogs/&lt;/code&gt; prepended) — the two values genuinely differ. Per spec, &lt;code&gt;overrides&lt;/code&gt; takes precedence over &lt;code&gt;defaults&lt;/code&gt;, so the prefix a client should actually use is &lt;code&gt;catalogs/123456789012&lt;/code&gt;. It's confirmed, from actual output, that &lt;code&gt;defaults&lt;/code&gt; and &lt;code&gt;overrides&lt;/code&gt; really can disagree.&lt;/p&gt;

&lt;p&gt;The other &lt;code&gt;defaults&lt;/code&gt; entries are worth a look too — &lt;code&gt;rest.sigv4-enabled&lt;/code&gt; and &lt;code&gt;rest.signing-name&lt;/code&gt; map directly onto the PyIceberg configuration items of the same name. &lt;code&gt;rest-table-scan-enabled&lt;/code&gt; and &lt;code&gt;rest-data-commit-enabled&lt;/code&gt; are presumably feature flags for server-side scan planning and write commits, discussed further below.&lt;/p&gt;

&lt;p&gt;Incidentally, this response also didn't include an &lt;code&gt;endpoints&lt;/code&gt; field.&lt;/p&gt;

&lt;p&gt;What you pass as &lt;code&gt;warehouse&lt;/code&gt; for the Glue endpoint is a Glue catalog ID (defaulting to the current account's root catalog if omitted). Where the S3 Tables endpoint's &lt;code&gt;warehouse&lt;/code&gt; was "a bucket ARN," here it's "where in the catalog hierarchy to connect."&lt;/p&gt;

&lt;h3&gt;
  
  
  Prefix Rules: Encoding the Catalog Hierarchy
&lt;/h3&gt;

&lt;p&gt;The Glue endpoint's prefix always takes the form &lt;code&gt;/catalogs/{catalog}&lt;/code&gt;. The official documentation defines the following rules for how &lt;code&gt;{catalog}&lt;/code&gt; is written:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Target&lt;/th&gt;
&lt;th&gt;Prefix notation&lt;/th&gt;
&lt;th&gt;Example REST path&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Default catalog of the current account&lt;/td&gt;
&lt;td&gt;&lt;code&gt;:&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;GET /v1/catalogs/:/namespaces&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Default catalog of a specific account&lt;/td&gt;
&lt;td&gt;Account ID&lt;/td&gt;
&lt;td&gt;&lt;code&gt;GET /v1/catalogs/123456789012/namespaces&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Nested catalog&lt;/td&gt;
&lt;td&gt;&lt;code&gt;catalog1:catalog2&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;GET /v1/catalogs/rmscatalog1:db1/namespaces&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Nested catalog of a specific account&lt;/td&gt;
&lt;td&gt;&lt;code&gt;accountId:catalog1:catalog2&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;GET /v1/catalogs/123456789012:s3tablescatalog:bucket/namespaces&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The &lt;code&gt;:&lt;/code&gt; notation for the default catalog is confusing at first glance, but it makes more sense once you think of it as "encoding the catalog hierarchy's separator as &lt;code&gt;:&lt;/code&gt; instead of the path separator &lt;code&gt;/&lt;/code&gt;." It's essentially using the free-form nature of the Iceberg REST spec's prefix to cram Glue's multi-catalog hierarchy into the URL.&lt;/p&gt;

&lt;h3&gt;
  
  
  Listing Namespaces (Default Catalog)
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;awscurl &lt;span class="nt"&gt;--service&lt;/span&gt; glue &lt;span class="nt"&gt;--region&lt;/span&gt; ap-northeast-1 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="s2"&gt;"https://glue.ap-northeast-1.amazonaws.com/iceberg/v1/catalogs/123456789012/namespaces"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Note&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The default catalog can be expressed either as a single colon (&lt;code&gt;:&lt;/code&gt;) or as the account ID. Here I'm using the &lt;code&gt;catalogs/{accountId}&lt;/code&gt; form, which is also what showed up in &lt;code&gt;overrides.prefix&lt;/code&gt; in the &lt;code&gt;getConfig&lt;/code&gt; response above.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Result (excerpt):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"namespaces"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"hive"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"icebergdb"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;What comes back is the familiar list of Glue databases — the response directly confirms the mapping "Glue database = Iceberg namespace." Notably, S3 Tables-backed databases under &lt;code&gt;s3tablescatalog&lt;/code&gt; are &lt;em&gt;not&lt;/em&gt; included here. The default catalog only covers Glue databases directly under the account; to see S3 Tables namespaces, you need to explicitly reach them via the nested catalog prefix, described below.&lt;/p&gt;

&lt;p&gt;Incidentally, the Glue endpoint has the same single-level namespace restriction as S3 Tables. What looks like a multi-level structure isn't expressed by making namespaces deeper — it's expressed by nesting catalogs. That seems to be a consistent design choice on Glue's part.&lt;/p&gt;

&lt;h3&gt;
  
  
  Reading an S3 Tables Table Through the Glue Endpoint
&lt;/h3&gt;

&lt;p&gt;This is the heart of today's investigation: reading the same table we read via the S3 Tables endpoint, this time through the Glue endpoint.&lt;/p&gt;

&lt;p&gt;S3 Tables with Glue integration enabled is mounted under the &lt;code&gt;s3tablescatalog&lt;/code&gt; federated catalog. In PyIceberg, you'd specify &lt;code&gt;warehouse&lt;/code&gt; as &lt;code&gt;123456789012:s3tablescatalog/penguin-rest-test&lt;/code&gt;. Applying the earlier prefix-conversion rule, the path becomes:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;awscurl &lt;span class="nt"&gt;--service&lt;/span&gt; glue &lt;span class="nt"&gt;--region&lt;/span&gt; ap-northeast-1 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="s2"&gt;"https://glue.ap-northeast-1.amazonaws.com/iceberg/v1/catalogs/123456789012:s3tablescatalog:penguin-rest-test/namespaces/analytics/tables/daily_sales"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Result (excerpt):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"config"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"createdBy"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"123456789012"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"s3TableArn"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"arn:aws:s3tables:ap-northeast-1:123456789012:bucket/penguin-rest-test/table/34c72c19-610d-4e5c-bee4-6ae9f093c583"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"ownerAccountId"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"123456789012"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"metadata_location"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"s3://34c72c19-610d-4e5c-d8d1qwx3db1p3tbr8g9jch9n6e14sapn1b--table-s3/metadata/00000-163a447a-c64d-44f9-8619-5db9561e549b.metadata.json"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"format"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"ICEBERG"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"warehouse_location"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"s3://34c72c19-610d-4e5c-d8d1qwx3db1p3tbr8g9jch9n6e14sapn1b--table-s3"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"table_type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"ICEBERG"&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"metadata"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"format-version"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"table-uuid"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"9507ea2b-7a71-40a7-b6a9-1d6e633955e1"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"current-schema-id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"schemas"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"struct"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"schema-id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"fields"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"sales_date"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"required"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"date"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"amount"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"required"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"long"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"current-snapshot-id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;-1&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"metadata-location"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"s3://34c72c19-610d-4e5c-d8d1qwx3db1p3tbr8g9jch9n6e14sapn1b--table-s3/metadata/00000-163a447a-c64d-44f9-8619-5db9561e549b.metadata.json"&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Note&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The nested-catalog prefix notation used above (&lt;code&gt;accountId:s3tablescatalog:bucketName&lt;/code&gt;) was derived from the conversion-rule table in the official documentation. Sending an actual request in this format resulted in correct routing, and both authorization and the underlying data checked out.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The &lt;code&gt;metadata-location&lt;/code&gt; and &lt;code&gt;table-uuid&lt;/code&gt; (&lt;code&gt;9507ea2b-7a71-40a7-b6a9-1d6e633955e1&lt;/code&gt;) here match exactly what we retrieved via the S3 Tables endpoint earlier. Different entry point, same underlying entity.&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;config&lt;/code&gt; contents are worth noting too. In this request — where we didn't set the &lt;code&gt;x-iceberg-access-delegation&lt;/code&gt; header — &lt;code&gt;config&lt;/code&gt; didn't contain S3 access credentials, only metadata like &lt;code&gt;s3TableArn&lt;/code&gt; (the original S3 Tables ARN) and &lt;code&gt;warehouse_location&lt;/code&gt; (the actual S3 path). As I'll cover below, one more configuration step was needed before credentials would actually come back.&lt;/p&gt;

&lt;p&gt;For the same table: through the S3 Tables endpoint the path is &lt;code&gt;/v1/{bucketArn}/namespaces/analytics/tables/daily_sales&lt;/code&gt;, while through the Glue endpoint it's &lt;code&gt;/v1/catalogs/{accountId}:s3tablescatalog:{bucketName}/namespaces/analytics/tables/daily_sales&lt;/code&gt;. Physically they point to the same &lt;code&gt;metadata.json&lt;/code&gt;, but the path structures are completely different because the two services have entirely different mental models of "what a catalog is." That, I think, is the essential difference between the two endpoints.&lt;/p&gt;

&lt;h3&gt;
  
  
  Access Control Differences (Lake Formation)
&lt;/h3&gt;

&lt;p&gt;Accessing via the Glue endpoint requires Lake Formation grants in addition to IAM policies (&lt;code&gt;glue:GetCatalog&lt;/code&gt;, &lt;code&gt;glue:GetTable&lt;/code&gt;, etc.), because S3 Tables tables get registered as Lake Formation resources when Glue integration is enabled.&lt;/p&gt;

&lt;p&gt;Further, for an external engine to read the actual data, you need to enable full-table access for external engines in Lake Formation, and allow the IAM role to call &lt;code&gt;lakeformation:GetDataAccess&lt;/code&gt;. This mechanism is what issues the temporary credentials known as vended credentials.&lt;/p&gt;

&lt;p&gt;In practice, though, this alone wasn't enough. It took explicitly registering the table bucket as a federated Lake Formation resource via &lt;code&gt;aws lakeformation register-resource --with-federation&lt;/code&gt;, &lt;em&gt;and&lt;/em&gt; setting up a trust policy so the Lake Formation service itself (&lt;code&gt;lakeformation.amazonaws.com&lt;/code&gt;) could assume the IAM role used for issuing credentials, before vended credentials actually started being issued. If this registration step is missing, requests with the header described below (even with otherwise-correct IAM policies and Lake Formation grants) just come back with a credential-less &lt;code&gt;config&lt;/code&gt; — only &lt;code&gt;s3TableArn&lt;/code&gt; and &lt;code&gt;warehouse_location&lt;/code&gt; — with no error at all, which makes it an easy thing to miss.&lt;/p&gt;

&lt;p&gt;When PyIceberg hits the Glue endpoint, it attaches this header to the request:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;x-iceberg-access-delegation: vended-credentials
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Per the Iceberg REST spec, this header signals to the server that the client wants credential delegation. However — at least in this environment — as long as the federation registration was complete, &lt;strong&gt;calling &lt;code&gt;LoadTable&lt;/code&gt; without this header still returned credentials the same way&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;In other words, what actually determines whether credentials come back doesn't seem to be the presence of the header, but whether the Lake Formation federation registration is complete. The header is the spec-compliant way to signal intent, but at least in this environment it wasn't the deciding factor in the Glue endpoint's behavior.&lt;/p&gt;

&lt;p&gt;Still, to verify things properly per spec, let's call &lt;code&gt;LoadTable&lt;/code&gt; with the header attached:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# With the vended-credentials header&lt;/span&gt;
awscurl &lt;span class="nt"&gt;--service&lt;/span&gt; glue &lt;span class="nt"&gt;--region&lt;/span&gt; ap-northeast-1 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"x-iceberg-access-delegation: vended-credentials"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="s2"&gt;"https://glue.ap-northeast-1.amazonaws.com/iceberg/v1/catalogs/123456789012:s3tablescatalog:penguin-rest-test/namespaces/analytics/tables/daily_sales"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Result (excerpt):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"metadata-location"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"s3://34c72c19-610d-4e5c-d8d1qwx3db1p3tbr8g9jch9n6e14sapn1b--table-s3/metadata/00000-163a447a-c64d-44f9-8619-5db9561e549b.metadata.json"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"metadata"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"..."&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"..."&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"config"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"s3.access-key-id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"(masked)"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"s3.secret-access-key"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"(masked)"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"s3.session-token"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"(masked)"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"s3.session-token-expires-at-ms"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"1783651228000"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"s3TableArn"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"arn:aws:s3tables:ap-northeast-1:123456789012:bucket/penguin-rest-test/table/34c72c19-610d-4e5c-bee4-6ae9f093c583"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"warehouse_location"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"s3://34c72c19-610d-4e5c-d8d1qwx3db1p3tbr8g9jch9n6e14sapn1b--table-s3"&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;s3.access-key-id&lt;/code&gt;, &lt;code&gt;s3.secret-access-key&lt;/code&gt;, and &lt;code&gt;s3.session-token&lt;/code&gt; are actually present here — these are the vended credentials. Along with &lt;code&gt;s3.session-token-expires-at-ms&lt;/code&gt; (session expiration in epoch milliseconds), this confirms from the response itself that the catalog is issuing a temporary storage key directly.&lt;/p&gt;

&lt;p&gt;The S3 Tables endpoint, meanwhile, has no notion of Lake Formation at all — authorization there is handled entirely via &lt;code&gt;s3tables:*&lt;/code&gt; IAM actions, and resource-based policies on the table bucket are also available.&lt;/p&gt;

&lt;p&gt;Same underlying table, but the authorization model changes depending on which entry point you go through — something worth keeping in mind during access-design work. Incidentally, exactly how far you can deliberately vary Lake Formation permissions and what happens as a result (does removing a grant actually deny access, etc.) feels like enough material for its own article, so I'll leave that for another time.&lt;/p&gt;




&lt;h1&gt;
  
  
  Summarizing the Differences Between the Two Endpoints
&lt;/h1&gt;

&lt;p&gt;Here's what actually turned up from hitting both endpoints directly.&lt;/p&gt;

&lt;h3&gt;
  
  
  Basic Structure
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Item&lt;/th&gt;
&lt;th&gt;Glue endpoint&lt;/th&gt;
&lt;th&gt;S3 Tables endpoint&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Endpoint&lt;/td&gt;
&lt;td&gt;&lt;code&gt;glue.&amp;lt;region&amp;gt;.amazonaws.com/iceberg&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;s3tables.&amp;lt;region&amp;gt;.amazonaws.com/iceberg&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Signing service name&lt;/td&gt;
&lt;td&gt;&lt;code&gt;glue&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;s3tables&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;warehouse&lt;/code&gt; value&lt;/td&gt;
&lt;td&gt;Glue catalog ID&lt;/td&gt;
&lt;td&gt;Table bucket ARN&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;prefix&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;/catalogs/{catalog}&lt;/code&gt; (hierarchy encoding)&lt;/td&gt;
&lt;td&gt;URL-encoded bucket ARN&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Catalog scope&lt;/td&gt;
&lt;td&gt;Account-wide (multi-catalog hierarchy)&lt;/td&gt;
&lt;td&gt;Single table bucket&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Storage targeted&lt;/td&gt;
&lt;td&gt;General-purpose S3 Iceberg + S3 Tables&lt;/td&gt;
&lt;td&gt;S3 Tables only&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Access control&lt;/td&gt;
&lt;td&gt;IAM + Lake Formation (hybrid possible)&lt;/td&gt;
&lt;td&gt;s3tables IAM actions only&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Credential vending&lt;/td&gt;
&lt;td&gt;Vended credentials (requires &lt;code&gt;register-resource --with-federation&lt;/code&gt;)&lt;/td&gt;
&lt;td&gt;None (uses the caller's own credentials)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h1&gt;
  
  
  Where to Use Which, and Gotchas
&lt;/h1&gt;

&lt;p&gt;The official documentation gives guidance on when to use each endpoint:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;If you only need basic read/write access to a single table bucket: the S3 Tables endpoint&lt;/li&gt;
&lt;li&gt;If you need to integrate multiple catalog sources, or need centralized governance and fine-grained access control via Lake Formation: the Glue endpoint&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;From actually hitting both directly, here are a few things I'd flag:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Getting the signing service name wrong (&lt;code&gt;glue&lt;/code&gt; / &lt;code&gt;s3tables&lt;/code&gt;) results in a 403. This is exactly the kind of mistake a wrong &lt;code&gt;rest.signing-name&lt;/code&gt; in your client config produces.&lt;/li&gt;
&lt;li&gt;Same table, different authorization model depending on which endpoint you go through (whether Lake Formation is involved). Be explicit up front about which path you're connecting through when designing permissions.&lt;/li&gt;
&lt;li&gt;CTAS isn't supported on either endpoint. You can work around this by splitting it into &lt;code&gt;CREATE TABLE&lt;/code&gt; + &lt;code&gt;INSERT INTO&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;dropTable&lt;/code&gt; on the S3 Tables endpoint requires &lt;code&gt;purge=true&lt;/code&gt;. Depending on your Spark version, &lt;code&gt;DROP TABLE PURGE&lt;/code&gt; can end up sending &lt;code&gt;purge=false&lt;/code&gt; anyway — in that case, you'll need to delete via the native &lt;code&gt;DeleteTable&lt;/code&gt; API instead.&lt;/li&gt;
&lt;/ul&gt;




&lt;h1&gt;
  
  
  Looking Ahead (Some Personal Thoughts)
&lt;/h1&gt;

&lt;p&gt;Hitting both endpoints directly left me with the impression that each service's design philosophy shows through in how it uses the "free-form" parts of the Iceberg REST Catalog spec.&lt;/p&gt;

&lt;p&gt;The S3 Tables endpoint — with its prefix being the bucket ARN, authorization handled through s3tables IAM actions, and CloudTrail logging it as a native API — looks, from this angle, like a thin translation layer over the storage API.&lt;/p&gt;

&lt;p&gt;The Glue endpoint, on the other hand, encodes a catalog hierarchy into the prefix, has Lake Formation stepping into authorization, and (when configured correctly) issues temporary storage keys via vended credentials. Both general-purpose S3-based Iceberg and S3 Tables are visible through the same entry point — which lines up with something I speculated in an earlier article, that Glue Data Catalog may be evolving into a metadata plane for AWS as a whole. That same idea seems to show up directly in how the REST API's paths are designed.&lt;/p&gt;

&lt;p&gt;The fact that authentication is SigV4 rather than the OAuth the spec assumes is another data point suggesting that how a vendor uses the "free" parts of a standard spec reveals something about its design thinking — probably not unique to the Iceberg REST Catalog.&lt;/p&gt;

&lt;p&gt;One more thing I noticed while reading the spec: the latest version defines server-side scan planning endpoints (&lt;code&gt;planTableScan&lt;/code&gt; and friends), where the server does the work of building a scan plan. Glue already has a separate extension endpoint (&lt;code&gt;https://glue.&amp;lt;region&amp;gt;.amazonaws.com/extensions&lt;/code&gt;) that independently offers server-side scan planning for Redshift Managed Storage — so it looks like a capability that existed as a proprietary extension is now being absorbed into the standard spec. That's an interesting trend — a proprietary extension leading the way before the standard catches up — and I'd like to compare Glue's extension API against the standard plan-related endpoints in a future article.&lt;/p&gt;

&lt;p&gt;Snowflake's Catalog-Linked Database (&lt;code&gt;CATALOG_API_TYPE = AWS_GLUE&lt;/code&gt;) connects to exactly this Glue endpoint, so seeing how this API gets called from the Snowflake side is another topic I want to dig into going forward.&lt;/p&gt;




&lt;h1&gt;
  
  
  Conclusion
&lt;/h1&gt;

&lt;p&gt;In this article, I hit the Iceberg REST Catalog directly to sort out the differences between the Glue Data Catalog and S3 Tables endpoints. It took some effort, but I came away with a clearer understanding and a few new insights.&lt;/p&gt;

&lt;p&gt;To summarize:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Design philosophy&lt;/strong&gt;: even though both implement the same Iceberg REST Catalog spec, Glue is an "entry point to multiple catalogs" while S3 Tables is an "entry point to a single table bucket" — a difference visible just from the URL paths.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Prefix design&lt;/strong&gt;: the spec's free-form &lt;code&gt;{prefix}&lt;/code&gt; is used by Glue to encode a catalog hierarchy (&lt;code&gt;/catalogs/{catalog}&lt;/code&gt;), and by S3 Tables to encode the table bucket ARN.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Authentication&lt;/strong&gt;: both use IAM SigV4. Getting the signing service name wrong (&lt;code&gt;glue&lt;/code&gt; / &lt;code&gt;s3tables&lt;/code&gt;) results in a 403.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Authorization model&lt;/strong&gt;: for the same table, the Glue endpoint goes through IAM + Lake Formation, while the S3 Tables endpoint uses only s3tables IAM actions — the model changes depending on the path.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Vended credentials&lt;/strong&gt;: temporary credentials aren't issued just by attaching the &lt;code&gt;x-iceberg-access-delegation: vended-credentials&lt;/code&gt; header — you also need to complete resource registration via &lt;code&gt;register-resource --with-federation&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Debugging tips&lt;/strong&gt;: inspecting the actual &lt;code&gt;CanonicalRequest&lt;/code&gt; and headers with &lt;code&gt;awscurl -v&lt;/code&gt;, using CloudTrail's &lt;code&gt;errorCode&lt;/code&gt; to triage error types, and pre-checking URL assembly with &lt;code&gt;echo&lt;/code&gt; were all useful, unglamorous techniques.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Day to day, relying on PyIceberg's or Spark's abstractions is more than enough. But looking at the raw HTTP requests once gives you a clearer mental map between each client configuration item and "that part of that request," which raises the resolution you get when triaging connection errors.&lt;/p&gt;

&lt;p&gt;I hope this article is useful to anyone trying to understand how the Iceberg REST Catalog actually works.&lt;/p&gt;

</description>
      <category>aws</category>
      <category>iceberg</category>
      <category>dataengineering</category>
    </item>
    <item>
      <title>Does Amazon S3 Tables Replace AWS Glue Data Catalog? Understanding Their Relationship</title>
      <dc:creator>Aki</dc:creator>
      <pubDate>Wed, 01 Jul 2026 14:16:20 +0000</pubDate>
      <link>https://dev.to/aws-builders/does-amazon-s3-tables-replace-aws-glue-data-catalog-understanding-their-relationship-104a</link>
      <guid>https://dev.to/aws-builders/does-amazon-s3-tables-replace-aws-glue-data-catalog-understanding-their-relationship-104a</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Original Japanese article&lt;/strong&gt;: &lt;a href="https://zenn.dev/penginpenguin/articles/e5fecafc778cd1" rel="noopener noreferrer"&gt;S3 TablesはGlue Data Catalogを置き換えるのか考えてみた&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h1&gt;
  
  
  Introduction
&lt;/h1&gt;

&lt;p&gt;I'm Aki, an AWS Community Builder (&lt;a href="https://x.com/jitepengin" rel="noopener noreferrer"&gt;@jitepengin&lt;/a&gt;).&lt;/p&gt;

&lt;p&gt;When I first started exploring Amazon S3 Tables, one question immediately came to mind:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;"Does this service eventually replace AWS Glue Data Catalog?"&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Perhaps not everyone has the same impression. However, because S3 Tables provides its own Iceberg REST Catalog endpoint and can create and manage namespaces and tables without integrating with Glue Data Catalog, I couldn't help but wonder about it.&lt;/p&gt;

&lt;p&gt;The official documentation uses the term &lt;strong&gt;"integration"&lt;/strong&gt;, which suggests that the relationship is more nuanced than a simple replacement. Still, it can be difficult to understand how these two services actually fit together.&lt;/p&gt;

&lt;p&gt;In this article, I'd like to organize the relationship between Amazon S3 Tables and AWS Glue Data Catalog based on the official AWS documentation.&lt;/p&gt;

&lt;p&gt;To summarize upfront:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Amazon S3 Tables does not replace AWS Glue Data Catalog. Instead, it should be viewed as a new managed table storage service that provides its own Iceberg REST Catalog while also integrating with Glue Data Catalog.&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h1&gt;
  
  
  Understanding the Roles of S3 Tables and Glue Data Catalog
&lt;/h1&gt;

&lt;p&gt;To begin with, Amazon S3 Tables is &lt;strong&gt;not&lt;/strong&gt; a replacement for Glue Data Catalog.&lt;/p&gt;

&lt;p&gt;At the same time, it is not entirely accurate to think of S3 Tables as "just storage."&lt;/p&gt;

&lt;p&gt;S3 Tables provides its own Iceberg REST Catalog endpoint:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;https://s3tables.&amp;lt;region&amp;gt;.amazonaws.com/iceberg
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Even without integrating with Glue Data Catalog, you can create, list, delete, and read/write namespaces and tables directly through this endpoint.&lt;/p&gt;

&lt;p&gt;The official documentation describes this standalone endpoint as being suitable for scenarios where you only need basic read/write access to a single table bucket. For other scenarios, AWS recommends using the Glue Iceberg REST endpoint, which provides:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Integrated table management&lt;/li&gt;
&lt;li&gt;Centralized governance&lt;/li&gt;
&lt;li&gt;Fine-grained access control&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The roles of the two services can be summarized as follows:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Service&lt;/th&gt;
&lt;th&gt;Role&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Amazon S3 Tables&lt;/td&gt;
&lt;td&gt;A storage layer that stores Iceberg table data and metadata while also providing an Iceberg REST Catalog for a single table bucket&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;AWS Glue Data Catalog&lt;/td&gt;
&lt;td&gt;A centralized metadata catalog for AWS tables and databases, including S3 Tables, providing unified access and governance across analytics services such as Athena and Redshift&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;In other words, the central catalog for governance across AWS analytics services is still Glue Data Catalog.&lt;/p&gt;

&lt;p&gt;S3 Tables is simply one of the catalog sources that can be integrated into Glue Data Catalog as a federated catalog.&lt;/p&gt;

&lt;p&gt;Considering recent features such as S3 Tables, Catalog Federation, S3 Metadata, and S3 Annotations, all of which bring various metadata sources under Glue Data Catalog, the role of Glue Data Catalog may become even more important in the future.&lt;/p&gt;

&lt;p&gt;If you only need to manage a single table bucket, the standalone REST Catalog provided by S3 Tables may be sufficient.&lt;/p&gt;

&lt;p&gt;However, if you need to work across multiple analytics services or multiple catalog sources, using Glue Data Catalog as the entry point is likely to be much easier operationally.&lt;/p&gt;




&lt;h1&gt;
  
  
  Understanding Federated Catalogs
&lt;/h1&gt;

&lt;p&gt;By integrating S3 Tables with Glue Data Catalog, you can use a single catalog to discover and query data in Amazon S3 data lakes and even join that data with S3 Tables.&lt;/p&gt;

&lt;p&gt;A federated catalog allows users to access metadata through Glue Data Catalog without needing to know where that metadata is actually stored.&lt;/p&gt;

&lt;p&gt;From a user's perspective, S3 Tables integration works in a similar way—you can access it as another catalog within Glue Data Catalog without needing to know where the underlying metadata physically resides.&lt;/p&gt;

&lt;p&gt;The integration maps S3 Tables resources into Glue Catalog objects as follows:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;An S3 table bucket becomes a Data Catalog catalog.&lt;/li&gt;
&lt;li&gt;An S3 namespace becomes an AWS Glue database.&lt;/li&gt;
&lt;li&gt;An S3 table becomes an AWS Glue table.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;When integration is enabled through the console, AWS automatically creates another layer on top called:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;s3tablescatalog
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;When you integrate S3 Tables with Data Catalog through the Amazon S3 console, AWS creates a federated catalog named &lt;code&gt;s3tablescatalog&lt;/code&gt;, which acts as the parent catalog for all existing and future S3 table buckets in that account and Region.&lt;/p&gt;

&lt;p&gt;From the perspective of query engines, the architecture looks like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Athena / Redshift / Glue ETL
              │
              ▼
      Glue Data Catalog
              │
              ├── Traditional Glue Catalog Tables
              ├── S3 Tables (via s3tablescatalog)
              └── External Catalogs (via Catalog Federation)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Regardless of where the metadata actually resides, all of these sources appear as tables under Glue Data Catalog.&lt;/p&gt;

&lt;p&gt;The hierarchy inside S3 Tables looks like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;s3tablescatalog (federated catalog)
        └── analytics-bucket (child catalog = S3 table bucket)
                └── sales (database = S3 namespace)
                        └── transactions (table = S3 table)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For example, if you have a table bucket named &lt;code&gt;analytics-bucket&lt;/code&gt; containing a namespace called &lt;code&gt;sales&lt;/code&gt; and a table called &lt;code&gt;transactions&lt;/code&gt;, it can conceptually be represented in Glue Data Catalog as:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;s3tablescatalog/analytics-bucket/sales/transactions
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In Athena SQL, the same table is referenced as:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="nv"&gt;"s3tablescatalog/analytics-bucket"&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nv"&gt;"sales"&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nv"&gt;"transactions"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The important thing to remember is that the parent catalog layer, &lt;code&gt;s3tablescatalog&lt;/code&gt;, sits in front of the table bucket.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Note:&lt;br&gt;
The four-level hierarchy described above applies to same-account scenarios.&lt;br&gt;
In cross-account scenarios, individual S3 table buckets must be mounted manually into Data Catalog, resulting in a three-part hierarchy.&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h1&gt;
  
  
  Trying It Out
&lt;/h1&gt;

&lt;p&gt;Let's verify how an S3 table bucket becomes visible from Glue Data Catalog and how it can be queried from Athena.&lt;/p&gt;

&lt;h2&gt;
  
  
  Creating a Table Bucket
&lt;/h2&gt;

&lt;p&gt;Create a table bucket in the console and enable the &lt;strong&gt;"Enable integration"&lt;/strong&gt; checkbox.&lt;/p&gt;

&lt;p&gt;If you're using the CLI:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;aws s3tables create-table-bucket
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;At this point, integration with Glue Data Catalog is automatically configured.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fexrba70qwtx6se8asa69.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fexrba70qwtx6se8asa69.png" width="800" height="382"&gt;&lt;/a&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  Querying from Athena
&lt;/h2&gt;

&lt;p&gt;To query the table from Athena, specify:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;s3tablescatalog/&amp;lt;table-bucket-name&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;as the catalog.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F0womxx6k2mey4ycml0gu.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F0womxx6k2mey4ycml0gu.png" width="800" height="301"&gt;&lt;/a&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  Viewing It from Glue Data Catalog
&lt;/h2&gt;

&lt;p&gt;When you open the Glue Data Catalog console, you'll see the table bucket under &lt;code&gt;s3tablescatalog&lt;/code&gt;, followed by its namespaces and tables.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fucrar7i19wg6z2571hat.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fucrar7i19wg6z2571hat.png" width="800" height="235"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;You can also view the schema information directly.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F8ov5y3a3iah9k2rviutg.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F8ov5y3a3iah9k2rviutg.png" width="800" height="229"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;One particularly nice aspect of this integration is that, without any special configuration, you can access S3 Tables data directly from the familiar Athena query editor and Glue Data Catalog console.&lt;/p&gt;




&lt;h1&gt;
  
  
  Existing Access Control Mechanisms Continue to Work
&lt;/h1&gt;

&lt;p&gt;From an access management perspective, the existing Glue Data Catalog and Lake Formation mechanisms continue to work.&lt;/p&gt;

&lt;p&gt;Data Catalog supports two access control modes for S3 Tables integration.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Mode&lt;/th&gt;
&lt;th&gt;Description&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;IAM Access Control&lt;/td&gt;
&lt;td&gt;Controls access to both S3 Tables and Data Catalog through IAM policies&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;AWS Lake Formation Access Control&lt;/td&gt;
&lt;td&gt;Uses Lake Formation permissions in addition to Glue IAM permissions and supports database-, table-, column-, and row-level security&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;One important detail concerns credentials when using Lake Formation.&lt;/p&gt;

&lt;p&gt;If a registered role is configured and credential vending is enabled, principals do not need direct S3 Tables IAM permissions.&lt;/p&gt;

&lt;p&gt;This is because Lake Formation issues credentials on behalf of the principal using the registered role.&lt;/p&gt;

&lt;p&gt;I have another article covering AWS Lake Formation in detail if you're interested.&lt;br&gt;
&lt;a href="https://dev.to/aws-builders/organizing-how-to-use-aws-lake-formation-4hhp"&gt;Organizing How to Use AWS Lake Formation&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Because you can migrate between access control modes as requirements evolve, a practical approach might be to start with IAM-only permissions and later move to Lake Formation for finer-grained control.&lt;/p&gt;


&lt;h1&gt;
  
  
  If Nothing Is Being Replaced, What Actually Changed?
&lt;/h1&gt;

&lt;p&gt;The position of Glue Data Catalog itself has not changed.&lt;/p&gt;

&lt;p&gt;What has changed is the operational layer around data management and table maintenance.&lt;/p&gt;

&lt;p&gt;Traditionally, Iceberg on S3 consisted of:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A general-purpose S3 bucket&lt;/li&gt;
&lt;li&gt;Glue Data Catalog&lt;/li&gt;
&lt;li&gt;Table maintenance mechanisms&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Even in the traditional architecture, Glue Table Optimizer can already manage:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Compaction&lt;/li&gt;
&lt;li&gt;Snapshot retention&lt;/li&gt;
&lt;li&gt;Orphan file deletion&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Therefore, the major difference between traditional Iceberg on S3 and S3 Tables is not necessarily the existence of these features, but rather that they are enabled by default.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Item&lt;/th&gt;
&lt;th&gt;Traditional Iceberg on S3&lt;/th&gt;
&lt;th&gt;S3 Tables&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Storage&lt;/td&gt;
&lt;td&gt;General-purpose S3 bucket&lt;/td&gt;
&lt;td&gt;Dedicated table bucket&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Catalog&lt;/td&gt;
&lt;td&gt;Glue Data Catalog&lt;/td&gt;
&lt;td&gt;Native Iceberg REST Catalog or Glue Data Catalog&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Compaction&lt;/td&gt;
&lt;td&gt;Glue Table Optimizer (manual enablement)&lt;/td&gt;
&lt;td&gt;Managed and enabled by default&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Snapshot retention&lt;/td&gt;
&lt;td&gt;Glue Table Optimizer (manual enablement)&lt;/td&gt;
&lt;td&gt;Managed and enabled by default&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Orphan file deletion&lt;/td&gt;
&lt;td&gt;Glue Table Optimizer (manual enablement)&lt;/td&gt;
&lt;td&gt;Managed and enabled by default&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Of course, S3 Tables also provides dedicated resource types, optimized pricing models, and specialized APIs.&lt;/p&gt;

&lt;p&gt;However, one of the biggest differences is the reduction in onboarding effort thanks to these capabilities being enabled from the start.&lt;/p&gt;

&lt;p&gt;From the perspective of Glue Data Catalog, S3 Tables is simply another catalog source integrated as a federated catalog.&lt;/p&gt;

&lt;p&gt;It does not require replacing existing crawler-based Glue Data Catalog environments. Both approaches can coexist.&lt;/p&gt;


&lt;h1&gt;
  
  
  When Should You Use It?
&lt;/h1&gt;

&lt;p&gt;S3 Tables integration seems particularly well suited for:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Building new Iceberg tables with maintenance enabled by default.&lt;/li&gt;
&lt;li&gt;Querying across multiple table buckets from Athena or Redshift.&lt;/li&gt;
&lt;li&gt;Leveraging Lake Formation's fine-grained access controls.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Things to keep in mind:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Cross-account scenarios require manual mounting.&lt;/li&gt;
&lt;li&gt;Query engines require the &lt;code&gt;s3tablescatalog&lt;/code&gt; parent catalog path.&lt;/li&gt;
&lt;li&gt;Existing crawler-based Glue Catalog environments do not need to be migrated to S3 Tables.&lt;/li&gt;
&lt;/ul&gt;


&lt;h1&gt;
  
  
  My Thoughts on the Future
&lt;/h1&gt;

&lt;p&gt;As more specialized storage services like S3 Tables emerge, I believe Glue Data Catalog may evolve beyond being merely a Hive Metastore-compatible catalog and become more of an AWS-wide metadata hub.&lt;/p&gt;

&lt;p&gt;Catalog Federation already allows external catalogs such as Snowflake Horizon Catalog and Databricks Unity Catalog to connect under Glue Data Catalog.&lt;/p&gt;

&lt;p&gt;This suggests a future where:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;The data can reside anywhere, but the catalog entry point is centralized in Glue Data Catalog.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The recently announced Amazon S3 Annotations feature also seems to support this direction.&lt;/p&gt;

&lt;p&gt;S3 Annotations allows rich, mutable, and queryable metadata to be attached directly to S3 objects.&lt;/p&gt;

&lt;p&gt;When annotation tables are enabled, S3 automatically indexes those annotations into fully managed Apache Iceberg tables that can be queried using Athena.&lt;/p&gt;

&lt;p&gt;Interestingly, the official examples reference:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="nv"&gt;"s3tablescatalog/aws-s3"&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nv"&gt;"b_my_media_bucket"&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nv"&gt;"annotation"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;which means that the &lt;code&gt;s3tablescatalog&lt;/code&gt; hierarchy is now appearing outside the context of S3 Tables itself.&lt;/p&gt;

&lt;p&gt;While S3 Tables turns data into Iceberg tables and integrates them into Glue Data Catalog, S3 Annotations appears to do something similar for object metadata.&lt;/p&gt;

&lt;p&gt;AWS has not explicitly stated this direction.&lt;/p&gt;

&lt;p&gt;However, when looking at S3 Tables, S3 Metadata, S3 Annotations, and Catalog Federation together, Glue Data Catalog increasingly looks like an AWS-wide metadata plane rather than simply a Hive Metastore-compatible service.&lt;/p&gt;

&lt;p&gt;It feels as though AWS is moving toward a future where both data and metadata can be accessed through a common Iceberg-based access model.&lt;/p&gt;

&lt;p&gt;If this trend continues, Glue Data Catalog may become even more important as the metadata plane for AWS data services.&lt;/p&gt;

&lt;p&gt;The arrival of S3 Tables does not diminish the importance of Glue Data Catalog.&lt;/p&gt;

&lt;p&gt;If anything, it clarifies its role as the hub that integrates multiple data sources and metadata sources.&lt;/p&gt;




&lt;h1&gt;
  
  
  Conclusion
&lt;/h1&gt;

&lt;p&gt;To summarize:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Amazon S3 Tables does not replace AWS Glue Data Catalog.&lt;/li&gt;
&lt;li&gt;S3 Tables integrates into Glue Data Catalog as a federated catalog.&lt;/li&gt;
&lt;li&gt;Glue Data Catalog may become even more important as a hub that integrates multiple metadata sources.&lt;/li&gt;
&lt;li&gt;The catalog hierarchy consists of:
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Federated Catalog (s3tablescatalog)
        ↓
Child Catalog (table bucket)
        ↓
Database (namespace)
        ↓
Table
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;Access control can be implemented using either IAM or Lake Formation and migrated later if necessary.&lt;/li&gt;
&lt;li&gt;The major change introduced by S3 Tables is that storage, metadata management, and table maintenance are now provided in a more integrated and managed manner through table buckets.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For building new AWS-native data lakes or lakehouses, S3 Tables is becoming a compelling option.&lt;/p&gt;

&lt;p&gt;However, this is not because it replaces Glue Data Catalog.&lt;/p&gt;

&lt;p&gt;Rather, it provides a more managed way to operate Iceberg tables on top of a metadata foundation centered around Glue Data Catalog.&lt;/p&gt;

&lt;p&gt;I hope this article helps clarify the relationship between Amazon S3 Tables and AWS Glue Data Catalog.&lt;/p&gt;

</description>
      <category>aws</category>
      <category>iceberg</category>
      <category>dataengineering</category>
    </item>
    <item>
      <title>Track Apache Iceberg Schema Changes in AWS Glue Data Catalog with aws glue get-table-versions</title>
      <dc:creator>Aki</dc:creator>
      <pubDate>Mon, 15 Jun 2026 00:08:31 +0000</pubDate>
      <link>https://dev.to/aws-builders/track-apache-iceberg-schema-changes-in-aws-glue-data-catalog-with-aws-glue-get-table-versions-246i</link>
      <guid>https://dev.to/aws-builders/track-apache-iceberg-schema-changes-in-aws-glue-data-catalog-with-aws-glue-get-table-versions-246i</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Original Japanese article&lt;/strong&gt;: &lt;a href="https://zenn.dev/penginpenguin/articles/2c0509ffa42d46" rel="noopener noreferrer"&gt;Iceberg × Glue Data Catalogのスキーマ変更履歴をaws glue get-table-versionsで確認する&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h1&gt;
  
  
  Introduction
&lt;/h1&gt;

&lt;p&gt;I'm Aki, an AWS Community Builder (&lt;a href="https://x.com/jitepengin" rel="noopener noreferrer"&gt;@jitepengin&lt;/a&gt;).&lt;/p&gt;

&lt;p&gt;As Apache Iceberg adoption continues to grow in AWS-based lakehouse architectures, schema evolution has become one of its most valuable features.&lt;/p&gt;

&lt;p&gt;At the same time, questions like the following inevitably arise:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;When did the schema change?&lt;/li&gt;
&lt;li&gt;Which columns were added or removed?&lt;/li&gt;
&lt;li&gt;Who made the change?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Although you can view historical schema versions from the AWS Glue console, investigating these details can be cumbersome.&lt;/p&gt;

&lt;p&gt;This is where &lt;code&gt;aws glue get-table-versions&lt;/code&gt; becomes useful.&lt;/p&gt;

&lt;p&gt;When your Apache Iceberg tables are managed through AWS Glue Data Catalog, this command allows you to retrieve schema change history over time.&lt;/p&gt;

&lt;p&gt;In this article, I'll walk through the basics of &lt;code&gt;get-table-versions&lt;/code&gt;, show how to extract column-level differences with &lt;code&gt;jq&lt;/code&gt;, and explain how to identify the person who made a change by combining the results with CloudTrail.&lt;/p&gt;




&lt;h2&gt;
  
  
  What is &lt;code&gt;get-table-versions&lt;/code&gt;?
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;aws glue get-table-versions&lt;/code&gt; is an AWS CLI command that retrieves historical versions of a table registered in AWS Glue Data Catalog.&lt;/p&gt;

&lt;p&gt;Every time a Glue table definition is updated, a new &lt;code&gt;VersionId&lt;/code&gt; is created. Each version stores the schema definition, partition information, table parameters, and other metadata.&lt;/p&gt;

&lt;p&gt;In addition to schema tracking, Iceberg-specific parameters such as &lt;code&gt;metadata_location&lt;/code&gt; are also recorded, making this command useful for Iceberg operational management.&lt;/p&gt;

&lt;h3&gt;
  
  
  Isn't the Console Enough?
&lt;/h3&gt;

&lt;p&gt;You can actually view previous schema versions from the AWS Glue console by selecting an older version from the version dropdown in the upper-right corner of the table page.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F25f1ou86n9haa1dzbso4.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F25f1ou86n9haa1dzbso4.png" width="800" height="416"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;However, the console only allows you to inspect one version at a time.&lt;/p&gt;

&lt;p&gt;It does not show differences between versions, making it difficult to answer questions such as:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Which columns were added or removed?&lt;/li&gt;
&lt;li&gt;In which version did the change occur?&lt;/li&gt;
&lt;li&gt;When exactly was the schema updated?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you need to compare multiple versions or quickly investigate schema-related incidents, using the CLI is far more efficient.&lt;/p&gt;

&lt;h3&gt;
  
  
  Basic Command Syntax
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;aws glue get-table-versions &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--database-name&lt;/span&gt; &amp;lt;database-name&amp;gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--table-name&lt;/span&gt; &amp;lt;table-name&amp;gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--no-paginate&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--region&lt;/span&gt; ap-northeast-1
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Using &lt;code&gt;--no-paginate&lt;/code&gt; retrieves all versions in a single request.&lt;/p&gt;

&lt;p&gt;The response contains a &lt;code&gt;TableVersions&lt;/code&gt; array. Each element includes:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;code&gt;VersionId&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;Table&lt;/code&gt; (full schema definition)&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;UpdateTime&lt;/code&gt; (timestamp when the Glue table definition was updated)&lt;/li&gt;
&lt;/ul&gt;

&lt;h4&gt;
  
  
  Example Output
&lt;/h4&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"Table"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"Name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"flights_1m"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"DatabaseName"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"icebergdb"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"UpdateTime"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"2026-06-12T05:21:18+00:00"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"StorageDescriptor"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"Columns"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="nl"&gt;"Name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"fl_date"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="nl"&gt;"Type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"date"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="nl"&gt;"Parameters"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="nl"&gt;"iceberg.field.current"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"true"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="nl"&gt;"iceberg.field.id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"1"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="nl"&gt;"iceberg.field.optional"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"true"&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"Parameters"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"metadata_location"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"s3://&amp;lt;your-bucket&amp;gt;/warehouse/flights_1m/metadata/00002-....metadata.json"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"previous_metadata_location"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"s3://&amp;lt;your-bucket&amp;gt;/warehouse/flights_1m/metadata/00001-....metadata.json"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"table_type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"ICEBERG"&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"VersionId"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"5"&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  Characteristics of Iceberg Tables
&lt;/h2&gt;

&lt;p&gt;Compared to standard Glue tables, Iceberg tables have several notable characteristics.&lt;/p&gt;

&lt;p&gt;First, you may notice that &lt;code&gt;VersionId&lt;/code&gt; values become surprisingly large.&lt;/p&gt;

&lt;p&gt;Glue &lt;code&gt;VersionId&lt;/code&gt; values are not the same as Iceberg Snapshot IDs or commit counts. They increment whenever the Glue table definition stored in the catalog is updated.&lt;/p&gt;

&lt;p&gt;Because Iceberg frequently updates &lt;code&gt;metadata_location&lt;/code&gt;, Glue table definitions are also updated regularly, causing &lt;code&gt;VersionId&lt;/code&gt; to increase much more rapidly than expected.&lt;/p&gt;

&lt;p&gt;In one of my test environments, a table had already reached &lt;code&gt;VersionId&lt;/code&gt; 318.&lt;/p&gt;

&lt;p&gt;However, most of those versions were created by metadata updates associated with data writes rather than actual schema changes.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Ftmxivtd2v729dl9qliep.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Ftmxivtd2v729dl9qliep.png" width="800" height="413"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Another notable characteristic is the presence of &lt;code&gt;iceberg.field.id&lt;/code&gt; within each column's &lt;code&gt;Parameters&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;This field represents Iceberg's internal column ID, which enables schema evolution features such as column renames without breaking data mapping.&lt;/p&gt;

&lt;p&gt;The table-level &lt;code&gt;Parameters&lt;/code&gt; section also contains:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;code&gt;metadata_location&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;previous_metadata_location&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;These point to Iceberg metadata files stored in Amazon S3.&lt;/p&gt;

&lt;p&gt;Because Glue &lt;code&gt;VersionId&lt;/code&gt; values correspond to Iceberg metadata updates, you can trace these files for deeper historical analysis when necessary.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Warning&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;AWS Glue Data Catalog has service quotas for the number of stored table versions.&lt;/p&gt;

&lt;p&gt;In Iceberg environments, it's possible to hit these limits and encounter &lt;code&gt;ResourceNumberLimitExceededException&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Consider periodically removing old versions or using the &lt;code&gt;SkipArchive&lt;/code&gt; option of &lt;code&gt;UpdateTable&lt;/code&gt; to reduce version growth.&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  Viewing Schema Change History
&lt;/h2&gt;

&lt;h3&gt;
  
  
  List Columns by Version
&lt;/h3&gt;

&lt;p&gt;Let's start by displaying the update timestamp and column list for each version.&lt;/p&gt;

&lt;p&gt;Using &lt;code&gt;jq&lt;/code&gt;, we can extract &lt;code&gt;VersionId&lt;/code&gt;, &lt;code&gt;UpdateTime&lt;/code&gt;, and the column names:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;aws glue get-table-versions &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--database-name&lt;/span&gt; icebergdb &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--table-name&lt;/span&gt; flights_1m &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--no-paginate&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--region&lt;/span&gt; ap-northeast-1 &lt;span class="se"&gt;\&lt;/span&gt;
| jq &lt;span class="nt"&gt;-r&lt;/span&gt; &lt;span class="s1"&gt;'
    .TableVersions[]
    | {
        version: .VersionId,
        updated: .Table.UpdateTime,
        columns: [.Table.StorageDescriptor.Columns[].Name]
      }
    | "\(.version)\t\(.updated)\t\(.columns | join(", "))"
  '&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Example output:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;5    2026-06-12T05:21:18+00:00    fl_date, dep_delay, arr_delay, air_time, distance, dep_time, arr_time
4    2026-06-12T05:20:48+00:00    fl_date, dep_delay, arr_delay, air_time, distance, dep_time, double
3    2026-06-12T05:20:11+00:00    fl_date, dep_delay, arr_delay, air_time, distance, dep_time
2    2025-09-01T21:27:05+00:00    fl_date, dep_delay, arr_delay, air_time, distance, dep_time, arr_time
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Note that &lt;code&gt;VersionId&lt;/code&gt; values are not necessarily consecutive.&lt;/p&gt;

&lt;p&gt;In this example they happen to be &lt;code&gt;2 → 3 → 4 → 5&lt;/code&gt;, but in production environments they may reach hundreds or even thousands.&lt;/p&gt;

&lt;p&gt;Since Glue &lt;code&gt;VersionId&lt;/code&gt; values do not directly correspond to Iceberg commits, you should not use them alone to estimate the number of schema changes.&lt;/p&gt;




&lt;h3&gt;
  
  
  Compare Differences Between Versions
&lt;/h3&gt;

&lt;p&gt;To identify added and removed columns between adjacent versions, you can compare column arrays using the &lt;code&gt;jq&lt;/code&gt; array difference (&lt;code&gt;-&lt;/code&gt;) operator.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;aws glue get-table-versions &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--database-name&lt;/span&gt; icebergdb &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--table-name&lt;/span&gt; flights_1m &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--no-paginate&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--region&lt;/span&gt; ap-northeast-1 &lt;span class="se"&gt;\&lt;/span&gt;
| jq &lt;span class="s1"&gt;'
    [ .TableVersions[] | {
        v: .VersionId,
        cols: [.Table.StorageDescriptor.Columns[].Name]
      }
    ]
    | sort_by(.v | tonumber)
    | . as $sorted
    | range(1; length)
    | {
        from: $sorted[.].v,
        added:   ($sorted[.].cols - $sorted[. - 1].cols),
        removed: ($sorted[. - 1].cols - $sorted[.].cols)
      }
  '&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Example output:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"from"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"3"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"added"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[],&lt;/span&gt;&lt;span class="w"&gt;           &lt;/span&gt;&lt;span class="nl"&gt;"removed"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"arr_time"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"from"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"4"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"added"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"double"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;   &lt;/span&gt;&lt;span class="nl"&gt;"removed"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"from"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"5"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"added"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"arr_time"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"removed"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"double"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This shows that:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;arr_time&lt;/code&gt; was removed between v2 and v3.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;double&lt;/code&gt; was added between v3 and v4.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;double&lt;/code&gt; was removed and &lt;code&gt;arr_time&lt;/code&gt; was restored between v4 and v5.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For full transparency, &lt;code&gt;double&lt;/code&gt; was not generated automatically by Glue.&lt;/p&gt;

&lt;p&gt;It was actually a mistake I made during testing. I intended to add the &lt;code&gt;arr_time&lt;/code&gt; column but accidentally entered the data type name &lt;code&gt;double&lt;/code&gt; as the column name.&lt;/p&gt;

&lt;p&gt;The issue was corrected in v5, but it serves as a useful demonstration that both mistakes and subsequent fixes are preserved in the version history.&lt;/p&gt;




&lt;h3&gt;
  
  
  Find Versions Containing a Specific Column
&lt;/h3&gt;

&lt;p&gt;If you need to answer a question such as:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"Which versions contained this column?"&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;you can use &lt;code&gt;select&lt;/code&gt; and &lt;code&gt;contains&lt;/code&gt; in &lt;code&gt;jq&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;TARGET_COLUMN&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"COLUMN_NAME"&lt;/span&gt;

aws glue get-table-versions &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--database-name&lt;/span&gt; icebergdb &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--table-name&lt;/span&gt; flights_1m &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--no-paginate&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--region&lt;/span&gt; ap-northeast-1 &lt;span class="se"&gt;\&lt;/span&gt;
| jq &lt;span class="nt"&gt;--arg&lt;/span&gt; col &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$TARGET_COLUMN&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="s1"&gt;'
    .TableVersions[]
    | select(
        .Table.StorageDescriptor.Columns
        | map(.Name)
        | contains([$col])
      )
    | {version: .VersionId, updated: .Table.UpdateTime}
  '&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Example output:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"version"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"4"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"updated"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"2026-06-12T05:20:48+00:00"&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This confirms that the column &lt;code&gt;double&lt;/code&gt; existed only in version 4.&lt;/p&gt;




&lt;h2&gt;
  
  
  Identify Who Changed the Schema with CloudTrail
&lt;/h2&gt;

&lt;p&gt;While &lt;code&gt;get-table-versions&lt;/code&gt; tells you when a schema changed, it does not tell you who made the change.&lt;/p&gt;

&lt;p&gt;To identify the responsible user or role, you can correlate the schema update time with CloudTrail &lt;code&gt;UpdateTable&lt;/code&gt; events.&lt;/p&gt;

&lt;p&gt;Once you've identified the relevant timestamp, search CloudTrail around that period:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;aws cloudtrail lookup-events &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--lookup-attributes&lt;/span&gt; &lt;span class="nv"&gt;AttributeKey&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;EventName,AttributeValue&lt;span class="o"&gt;=&lt;/span&gt;UpdateTable &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--start-time&lt;/span&gt; &lt;span class="s2"&gt;"2026-06-12T05:20:18+00:00"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--end-time&lt;/span&gt; &lt;span class="s2"&gt;"2026-06-12T05:21:18+00:00"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--region&lt;/span&gt; ap-northeast-1 &lt;span class="se"&gt;\&lt;/span&gt;
| jq &lt;span class="s1"&gt;'.Events[] | {time: .EventTime, user: .Username, detail: .CloudTrailEvent | fromjson | .requestParameters}'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Example output:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"time"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"2026-06-12T05:21:18+00:00"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"user"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"XXXXX"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"detail"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"catalogId"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"123456789012"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"databaseName"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"icebergdb"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"tableInput"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"flights_1m"&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;user&lt;/code&gt; field identifies the IAM user or role responsible for the update.&lt;/p&gt;

&lt;p&gt;Additionally, &lt;code&gt;detail.tableInput&lt;/code&gt; contains the updated table definition, allowing you to inspect the actual schema change directly from CloudTrail.&lt;/p&gt;

&lt;p&gt;In many cases, reviewing &lt;code&gt;UpdateTable&lt;/code&gt; events is sufficient.&lt;/p&gt;

&lt;p&gt;However, depending on the tool or workflow being used, changes may also appear as:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;code&gt;CreateTable&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;BatchCreatePartition&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Lake Formation-related events&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you cannot find the expected event, try expanding your search criteria.&lt;/p&gt;




&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;In this article, we explored how to use &lt;code&gt;aws glue get-table-versions&lt;/code&gt; to track schema changes in AWS Glue Data Catalog.&lt;/p&gt;

&lt;p&gt;With this approach, you can:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Review schema history chronologically&lt;/li&gt;
&lt;li&gt;Compare column additions and removals between versions&lt;/li&gt;
&lt;li&gt;Identify which versions contained specific columns&lt;/li&gt;
&lt;li&gt;Determine who made a change by correlating with CloudTrail&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Glue Data Catalog is often viewed simply as a metadata catalog for current table definitions.&lt;/p&gt;

&lt;p&gt;However, by leveraging Table Versions, it can also serve as a lightweight audit mechanism.&lt;/p&gt;

&lt;p&gt;Because schema evolution is a fundamental feature of Apache Iceberg, understanding how to answer questions such as:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;When did the schema change?&lt;/li&gt;
&lt;li&gt;What changed?&lt;/li&gt;
&lt;li&gt;Who changed it?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;can be extremely valuable during troubleshooting and day-to-day operations.&lt;/p&gt;

&lt;p&gt;I hope this article helps anyone managing Apache Iceberg tables with AWS Glue Data Catalog.&lt;/p&gt;

</description>
      <category>aws</category>
      <category>dataengineering</category>
      <category>iceberg</category>
    </item>
    <item>
      <title>Organizing How to Use AWS Lake Formation</title>
      <dc:creator>Aki</dc:creator>
      <pubDate>Mon, 08 Jun 2026 02:24:47 +0000</pubDate>
      <link>https://dev.to/aws-builders/organizing-how-to-use-aws-lake-formation-4hhp</link>
      <guid>https://dev.to/aws-builders/organizing-how-to-use-aws-lake-formation-4hhp</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Original Japanese article&lt;/strong&gt;: &lt;a href="https://zenn.dev/penginpenguin/articles/5852201b5da278" rel="noopener noreferrer"&gt;AWS Lake Formationの使い方について整理してみる&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h1&gt;
  
  
  Introduction
&lt;/h1&gt;

&lt;p&gt;I'm Aki, an AWS Community Builder (&lt;a href="https://x.com/jitepengin" rel="noopener noreferrer"&gt;@jitepengin&lt;/a&gt;).&lt;/p&gt;

&lt;p&gt;Previously, I wrote an article titled &lt;a href="https://dev.to/aws-builders/is-aws-glue-data-catalog-sufficient-as-a-data-catalog-organizing-its-design-limitations-and-kih"&gt;Is AWS Glue Data Catalog Sufficient as a Data Catalog? Organizing Its Design, Limitations, and Complementary Strategies&lt;/a&gt;.&lt;br&gt;
In that article, I mentioned that &lt;em&gt;"AWS Lake Formation is necessary to complement data governance"&lt;/em&gt; but did not go into detail because it was outside the scope of the article.&lt;/p&gt;

&lt;p&gt;This time, I'd like to organize my thoughts on Lake Formation, covering everything from the fundamentals to practical usage patterns.&lt;/p&gt;

&lt;p&gt;Lake Formation is often perceived as a service that is "somewhat difficult" or "unnecessary because IAM is enough."&lt;br&gt;
However, once you start implementing proper access control for a data lake, the necessity of Lake Formation becomes much clearer.&lt;/p&gt;

&lt;p&gt;I hope this article helps you evaluate whether Lake Formation is worth adopting in your environment.&lt;/p&gt;


&lt;h2&gt;
  
  
  What Is Lake Formation?
&lt;/h2&gt;

&lt;p&gt;AWS Lake Formation is a service that provides access management and governance for data lakes.&lt;/p&gt;

&lt;p&gt;It allows you to centrally manage &lt;strong&gt;who&lt;/strong&gt; can access &lt;strong&gt;which data&lt;/strong&gt; and at &lt;strong&gt;what level&lt;/strong&gt;.&lt;br&gt;
One of its key strengths is the ability to manage access controls consistently across multiple AWS services such as Athena, Glue, and Redshift Spectrum.&lt;/p&gt;

&lt;p&gt;Although they are often confused, Lake Formation and Glue Data Catalog serve different purposes.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Service&lt;/th&gt;
&lt;th&gt;Role&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Glue Data Catalog&lt;/td&gt;
&lt;td&gt;A technical catalog that manages metadata such as schemas and partitions&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Lake Formation&lt;/td&gt;
&lt;td&gt;A governance layer that manages access permissions for data registered in the Glue Data Catalog&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;


&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Amazon S3 (Actual Data)
        ↓
Glue Data Catalog (Metadata Management)
        ↓
Lake Formation (Access Control)
        ↓
Athena / Glue Job / Redshift Spectrum
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;In other words, data resides in S3, Glue Data Catalog manages metadata, and Lake Formation provides access control on top of that metadata layer.&lt;/p&gt;


&lt;h2&gt;
  
  
  How Lake Formation Differs from IAM
&lt;/h2&gt;
&lt;h3&gt;
  
  
  Isn't IAM Enough?
&lt;/h3&gt;

&lt;p&gt;When managing a data lake on S3 using IAM alone, several challenges emerge:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Granularity limitations&lt;/strong&gt;: IAM primarily operates at the bucket or prefix level, making table-, column-, and row-level access control difficult.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Operational complexity&lt;/strong&gt;: As users and roles increase, S3 bucket policies and IAM policies become increasingly difficult to manage.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Cross-account sharing&lt;/strong&gt;: Implementing data sharing across AWS accounts using only IAM can lead to complicated designs.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Limited visibility for auditing&lt;/strong&gt;: It is difficult to easily understand who can access which tables.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Typical examples include:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;More than ten Athena users need different levels of access, making permission management increasingly complicated.&lt;/li&gt;
&lt;li&gt;Different departments should see different subsets of data. For example, the sales department should only see Eastern Japan sales, while executives can see all data.&lt;/li&gt;
&lt;li&gt;Personally identifiable information (PII) such as email addresses and credit card numbers should be hidden from analysts.&lt;/li&gt;
&lt;li&gt;Data needs to be shared with another AWS account.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Lake Formation addresses these challenges.&lt;/p&gt;
&lt;h3&gt;
  
  
  What Lake Formation Solves
&lt;/h3&gt;

&lt;p&gt;With Lake Formation, you can implement:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Fine-grained table-, column-, and row-level access control&lt;/li&gt;
&lt;li&gt;Permission management at the Glue Data Catalog database and table level&lt;/li&gt;
&lt;li&gt;Tag-based access control (LF-TBAC) for large-scale environments&lt;/li&gt;
&lt;li&gt;Cross-account data sharing through AWS RAM&lt;/li&gt;
&lt;li&gt;Centralized auditing through CloudTrail integration&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;
  
  
  The Relationship Between IAM and Lake Formation
&lt;/h3&gt;

&lt;p&gt;Lake Formation does not replace IAM; it works as an additional layer on top of IAM.&lt;/p&gt;

&lt;p&gt;When a query is executed (for example, through Athena), access is granted only if both conditions are satisfied:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;IAM Permission
        AND
Lake Formation Permission
        ↓
Access Allowed
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Even if permissions are granted in Lake Formation, access is denied if IAM blocks it.&lt;/p&gt;

&lt;p&gt;Likewise, even if IAM allows access, the request is denied if the corresponding Lake Formation permissions are missing.&lt;/p&gt;

&lt;p&gt;Understanding this "AND" relationship is the foundation of permission design.&lt;/p&gt;




&lt;h2&gt;
  
  
  Lake Formation Permission Model
&lt;/h2&gt;

&lt;p&gt;Lake Formation permissions are managed across multiple levels.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Level&lt;/th&gt;
&lt;th&gt;Target&lt;/th&gt;
&lt;th&gt;Example Permissions&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Data Lake Administrator&lt;/td&gt;
&lt;td&gt;Entire Lake Formation environment&lt;/td&gt;
&lt;td&gt;Full permissions&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Database Level&lt;/td&gt;
&lt;td&gt;Glue Data Catalog database&lt;/td&gt;
&lt;td&gt;CREATE TABLE, DROP&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Table Level&lt;/td&gt;
&lt;td&gt;Individual table&lt;/td&gt;
&lt;td&gt;SELECT, INSERT, ALTER&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Column Level&lt;/td&gt;
&lt;td&gt;Specific columns within a table&lt;/td&gt;
&lt;td&gt;SELECT on selected columns&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Row Level&lt;/td&gt;
&lt;td&gt;Rows matching specific conditions&lt;/td&gt;
&lt;td&gt;SELECT on filtered rows&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Permissions can be granted or revoked through the console, CLI, or SDK.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Example: Grant SELECT permission on a table&lt;/span&gt;
aws lakeformation grant-permissions &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--principal&lt;/span&gt; &lt;span class="nv"&gt;DataLakePrincipalIdentifier&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;arn:aws:iam::123456789:role/analyst-role &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--permissions&lt;/span&gt; SELECT &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--resource&lt;/span&gt; &lt;span class="s1"&gt;'{
    "Table": {
      "DatabaseName": "mydb",
      "Name": "sales_table"
    }
  }'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  Column-Level and Row-Level Access Control
&lt;/h2&gt;

&lt;p&gt;One of Lake Formation's strongest capabilities is fine-grained access control beyond the table level.&lt;/p&gt;

&lt;p&gt;Both column-level and row-level security are implemented using a mechanism called &lt;strong&gt;Data Filters&lt;/strong&gt;.&lt;br&gt;
You create Data Filters in the console and reference them when granting permissions.&lt;/p&gt;
&lt;h3&gt;
  
  
  Column-Level Security
&lt;/h3&gt;

&lt;p&gt;Access can be restricted to specific columns.&lt;/p&gt;

&lt;p&gt;Suppose the &lt;code&gt;customer&lt;/code&gt; table contains the following columns:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;customer_id&lt;/th&gt;
&lt;th&gt;name&lt;/th&gt;
&lt;th&gt;email&lt;/th&gt;
&lt;th&gt;credit_card&lt;/th&gt;
&lt;th&gt;purchase_amount&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;You could allow analysts to access only &lt;code&gt;customer_id&lt;/code&gt;, &lt;code&gt;name&lt;/code&gt;, and &lt;code&gt;purchase_amount&lt;/code&gt;, while hiding &lt;code&gt;email&lt;/code&gt; and &lt;code&gt;credit_card&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;This can be achieved simply by specifying included or excluded columns in a Data Filter.&lt;br&gt;
Excluded columns will not appear in Athena query results.&lt;/p&gt;
&lt;h3&gt;
  
  
  Row Filters
&lt;/h3&gt;

&lt;p&gt;Row-level filters allow access only to rows matching specific conditions.&lt;/p&gt;

&lt;p&gt;Filter expressions are written using PartiQL WHERE-clause syntax.&lt;/p&gt;

&lt;p&gt;For example, if the &lt;code&gt;sales&lt;/code&gt; table contains a &lt;code&gt;region&lt;/code&gt; column and the Eastern Japan team should only see rows where &lt;code&gt;region = 'east'&lt;/code&gt;, you can create the following Data Filter:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;aws lakeformation create-data-cells-filter &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--table-data&lt;/span&gt; &lt;span class="s1"&gt;'{
    "TableCatalogId": "123456789012",
    "DatabaseName": "mydb",
    "TableName": "sales",
    "Name": "east-region-filter",
    "RowFilter": {
      "FilterExpression": "region = '&lt;/span&gt;&lt;span class="se"&gt;\'&lt;/span&gt;&lt;span class="s1"&gt;'east'&lt;/span&gt;&lt;span class="se"&gt;\'&lt;/span&gt;&lt;span class="s1"&gt;'"
    },
    "ColumnWildcard": {}
  }'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Combining column filters and row filters enables &lt;strong&gt;cell-level security&lt;/strong&gt;, where users can access only specific columns within specific rows.&lt;/p&gt;

&lt;h3&gt;
  
  
  Data Filter Limitations
&lt;/h3&gt;

&lt;p&gt;According to the official documentation:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Up to 100 filters per principal&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;array&lt;/code&gt; and &lt;code&gt;map&lt;/code&gt; types are not supported in filter expressions (&lt;code&gt;struct&lt;/code&gt; types can be used in row filters)&lt;/li&gt;
&lt;li&gt;Cell-level security does not support nested columns, views, or resource links&lt;/li&gt;
&lt;li&gt;Cell-level security is available in all regions when using Athena Engine Version 3 or Redshift Spectrum&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Common Use Cases
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Protecting PII such as email addresses and credit card numbers&lt;/li&gt;
&lt;li&gt;Restricting business data by department or geographic region&lt;/li&gt;
&lt;li&gt;Compliance requirements for regulated data&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Tag-Based Access Control (LF-TBAC)
&lt;/h2&gt;

&lt;p&gt;As the number of databases and tables grows, managing permissions table by table becomes increasingly difficult.&lt;/p&gt;

&lt;p&gt;LF-TBAC (Lake Formation Tag-Based Access Control) addresses this problem.&lt;/p&gt;

&lt;h3&gt;
  
  
  What Are LF-Tags?
&lt;/h3&gt;

&lt;p&gt;LF-Tags are key-value tags unique to Lake Formation.&lt;/p&gt;

&lt;p&gt;They are separate from both S3 resource tags and IAM tags and are managed independently within Lake Formation.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;aws lakeformation create-lf-tag &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--tag-key&lt;/span&gt; &lt;span class="s2"&gt;"sensitivity"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--tag-values&lt;/span&gt; &lt;span class="s1"&gt;'["public", "internal", "confidential"]'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Tagging Resources and Mapping Permissions
&lt;/h3&gt;

&lt;p&gt;LF-Tags can be assigned to databases, tables, and columns.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;aws lakeformation add-lf-tags-to-resource &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--resource&lt;/span&gt; &lt;span class="s1"&gt;'{"Table": {"DatabaseName": "mydb", "Name": "sales"}}'&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--lf-tags&lt;/span&gt; &lt;span class="s1"&gt;'[{"TagKey": "sensitivity", "TagValues": ["internal"]}]'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Permissions are then granted based on tags rather than table names.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;aws lakeformation grant-permissions &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--principal&lt;/span&gt; &lt;span class="nv"&gt;DataLakePrincipalIdentifier&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;arn:aws:iam::123456789:role/analyst-role &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--permissions&lt;/span&gt; SELECT &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--resource&lt;/span&gt; &lt;span class="s1"&gt;'{
    "LFTagPolicy": {
      "ResourceType": "TABLE",
      "Expression": [{"TagKey": "sensitivity", "TagValues": ["public", "internal"]}]
    }
  }'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This grants SELECT access to all tables tagged with either &lt;code&gt;sensitivity=public&lt;/code&gt; or &lt;code&gt;sensitivity=internal&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;When new tables are created, simply assigning the appropriate LF-Tag automatically applies the correct permissions.&lt;/p&gt;

&lt;h3&gt;
  
  
  Benefits in Large-Scale Environments
&lt;/h3&gt;

&lt;p&gt;In environments with dozens or hundreds of tables, table-by-table permission management becomes unrealistic.&lt;/p&gt;

&lt;p&gt;LF-TBAC enables a simpler model:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Roles can access data with specific tags.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;However, tag design should be carefully planned from the beginning.&lt;br&gt;
Defining categories such as &lt;code&gt;sensitivity&lt;/code&gt;, &lt;code&gt;domain&lt;/code&gt;, and &lt;code&gt;owner&lt;/code&gt; early on can save significant effort later.&lt;/p&gt;


&lt;h2&gt;
  
  
  Integration with Glue Data Catalog
&lt;/h2&gt;

&lt;p&gt;Lake Formation works closely with Glue Data Catalog.&lt;/p&gt;

&lt;p&gt;Glue Data Catalog manages metadata, while Lake Formation governs access to that metadata.&lt;br&gt;
Together they enable secure sharing and consumption of data stored in S3.&lt;/p&gt;
&lt;h3&gt;
  
  
  How Lake Formation Works with Data Catalog
&lt;/h3&gt;

&lt;p&gt;When Lake Formation is enabled, access to Glue Data Catalog is routed through Lake Formation authorization checks.&lt;/p&gt;

&lt;p&gt;This means that access to metadata itself—such as table definitions—can also be controlled.&lt;/p&gt;
&lt;h3&gt;
  
  
  Granting Lake Formation Permissions to Glue Jobs
&lt;/h3&gt;

&lt;p&gt;When a Glue Job accesses data governed by Lake Formation, permissions must be granted not only through IAM but also through Lake Formation.&lt;/p&gt;

&lt;p&gt;This is a common pitfall.&lt;/p&gt;

&lt;p&gt;A typical issue is:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;IAM permissions look correct, but the Glue Job still cannot read data.&lt;br&gt;
&lt;/p&gt;


&lt;/blockquote&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;aws lakeformation grant-permissions &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--principal&lt;/span&gt; &lt;span class="nv"&gt;DataLakePrincipalIdentifier&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;arn:aws:iam::123456789:role/glue-job-role &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--permissions&lt;/span&gt; SELECT &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--resource&lt;/span&gt; &lt;span class="s1"&gt;'{
    "Table": {"DatabaseName": "mydb", "Name": "source_table"}
  }'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Cross-Account Sharing
&lt;/h3&gt;

&lt;p&gt;Lake Formation supports cross-account data sharing through AWS RAM (Resource Access Manager).&lt;/p&gt;

&lt;p&gt;Users in the target account can query shared tables directly from their own Athena environment.&lt;/p&gt;

&lt;p&gt;Because Lake Formation permissions—including column and row filters—remain enforced, scenarios such as sharing data while excluding sensitive columns are supported.&lt;/p&gt;

&lt;p&gt;To use cross-account sharing, the Data Catalog Cross Account Version setting must be configured to Version 3 or later.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Version 3 enables direct sharing with IAM principals in other accounts.&lt;/li&gt;
&lt;li&gt;Version 4 adds support for hybrid access mode in cross-account scenarios.&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Integration with Athena and Redshift Spectrum
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Authorization Flow During Query Execution
&lt;/h3&gt;

&lt;p&gt;When Athena accesses a Lake Formation-managed table:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;A user executes a query in Athena.&lt;/li&gt;
&lt;li&gt;Athena requests table metadata from Glue Data Catalog.&lt;/li&gt;
&lt;li&gt;Lake Formation validates permissions.&lt;/li&gt;
&lt;li&gt;If authorized, access to data in S3 is allowed.&lt;/li&gt;
&lt;li&gt;Column and row filters are applied before results are returned.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;This enables fine-grained access control without modifying S3 bucket policies.&lt;/p&gt;

&lt;h3&gt;
  
  
  Redshift Spectrum Integration
&lt;/h3&gt;

&lt;p&gt;Since Redshift Spectrum also relies on Glue Data Catalog, Lake Formation permissions are enforced there as well.&lt;/p&gt;

&lt;p&gt;This makes it easier to maintain consistent access control across Athena and Redshift Spectrum.&lt;/p&gt;




&lt;h2&gt;
  
  
  Adoption Challenges and Realistic Operations
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Existing Environments: IAMAllowedPrincipals and Hybrid Access Mode
&lt;/h3&gt;

&lt;p&gt;To preserve backward compatibility, Lake Formation grants the &lt;code&gt;IAMAllowedPrincipals&lt;/code&gt; group &lt;code&gt;Super&lt;/code&gt; permissions on existing Data Catalog resources by default.&lt;/p&gt;

&lt;p&gt;In this state, access is effectively controlled by IAM alone, and Lake Formation's fine-grained controls are not enforced.&lt;/p&gt;

&lt;p&gt;To fully leverage Lake Formation, these permissions must eventually be removed and replaced with explicit Lake Formation permissions.&lt;/p&gt;

&lt;p&gt;However, switching everything at once can break existing workloads.&lt;/p&gt;

&lt;p&gt;This is where &lt;strong&gt;Hybrid Access Mode&lt;/strong&gt; becomes useful.&lt;/p&gt;

&lt;p&gt;When registering S3 locations, Hybrid Access Mode allows selected principals to opt into Lake Formation authorization while other principals continue using IAM-only access.&lt;/p&gt;

&lt;p&gt;This approach minimizes risk and enables gradual migration.&lt;/p&gt;

&lt;p&gt;Personally, I believe this is the most practical approach for existing environments.&lt;/p&gt;

&lt;h3&gt;
  
  
  Common Pitfalls
&lt;/h3&gt;

&lt;h4&gt;
  
  
  Forgetting Lake Formation Permissions for Glue Jobs
&lt;/h4&gt;

&lt;p&gt;As mentioned earlier, forgetting to grant Lake Formation permissions to Glue Job roles prevents ETL jobs from reading or writing data.&lt;/p&gt;

&lt;p&gt;Many "it should work but doesn't" permission issues ultimately trace back to this.&lt;/p&gt;

&lt;p&gt;I've forgotten it myself a few times and ended up scrambling to find the root cause.&lt;/p&gt;

&lt;h4&gt;
  
  
  Interaction with S3 Bucket Policies
&lt;/h4&gt;

&lt;p&gt;Lake Formation does not override S3 bucket policies.&lt;/p&gt;

&lt;p&gt;Even if access is granted in Lake Formation, requests are denied if the bucket policy blocks them.&lt;/p&gt;

&lt;p&gt;When adopting Lake Formation, bucket policies must be designed to allow access through Lake Formation-authorized service roles.&lt;/p&gt;

&lt;p&gt;Maintaining consistency among IAM, Lake Formation, and S3 bucket policies is critical.&lt;/p&gt;

&lt;p&gt;Changing the design later can become painful, so it's worth thinking through carefully from the beginning.&lt;/p&gt;

&lt;h4&gt;
  
  
  Configuring Data Lake Administrators
&lt;/h4&gt;

&lt;p&gt;When enabling Lake Formation for the first time, at least one Data Lake Administrator must be configured.&lt;/p&gt;

&lt;p&gt;Relying on a single administrator can become an operational bottleneck, so I recommend assigning multiple administrators.&lt;/p&gt;

&lt;h4&gt;
  
  
  Athena Workgroups
&lt;/h4&gt;

&lt;p&gt;When Athena Workgroups are used together with Lake Formation, behavior may vary depending on Workgroup configuration.&lt;/p&gt;

&lt;p&gt;In particular, don't forget to grant permissions to the S3 bucket used for query results.&lt;/p&gt;

&lt;p&gt;This is another thing I occasionally forget myself.&lt;/p&gt;

&lt;h3&gt;
  
  
  Incremental Adoption Strategy
&lt;/h3&gt;

&lt;p&gt;For new environments, enabling Lake Formation from the start is usually the best option.&lt;/p&gt;

&lt;p&gt;For existing environments, a phased approach tends to work better.&lt;/p&gt;

&lt;p&gt;I've done this before, and while it's certainly possible, it's somewhat tedious.&lt;br&gt;
If you're building a new environment, enabling Lake Formation from day one can save you trouble later.&lt;/p&gt;

&lt;h4&gt;
  
  
  Step 1: Gradual Opt-In with Hybrid Access Mode
&lt;/h4&gt;

&lt;ul&gt;
&lt;li&gt;Register S3 locations using Hybrid Access Mode&lt;/li&gt;
&lt;li&gt;Opt in selected principals&lt;/li&gt;
&lt;li&gt;Keep IAM-only access for others&lt;/li&gt;
&lt;li&gt;Monitor access through CloudTrail&lt;/li&gt;
&lt;/ul&gt;

&lt;h4&gt;
  
  
  Step 2: Use Lake Formation for New Tables
&lt;/h4&gt;

&lt;ul&gt;
&lt;li&gt;Manage permissions for newly created tables through Lake Formation&lt;/li&gt;
&lt;li&gt;Leave existing tables under IAMAllowedPrincipals&lt;/li&gt;
&lt;/ul&gt;

&lt;h4&gt;
  
  
  Step 3: Migrate Existing Tables
&lt;/h4&gt;

&lt;ul&gt;
&lt;li&gt;Gradually revoke IAMAllowedPrincipals permissions&lt;/li&gt;
&lt;li&gt;Replace them with Lake Formation permissions&lt;/li&gt;
&lt;li&gt;Validate behavior after each migration step&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Where Lake Formation Excels—and Where It Doesn't
&lt;/h2&gt;

&lt;p&gt;Lake Formation is particularly valuable for:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Fine-grained table-, column-, and row-level access control&lt;/li&gt;
&lt;li&gt;Consistent authorization across Athena, Glue, and Redshift Spectrum&lt;/li&gt;
&lt;li&gt;Scalable permission management using LF-TBAC&lt;/li&gt;
&lt;li&gt;Cross-account data sharing&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;However, some areas remain outside its scope:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Direct access control to raw files in S3&lt;/li&gt;
&lt;li&gt;Business metadata management&lt;/li&gt;
&lt;li&gt;Data quality management&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Relationship with Amazon DataZone
&lt;/h3&gt;

&lt;p&gt;As discussed in my previous article, Lake Formation and DataZone have complementary responsibilities.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Service&lt;/th&gt;
&lt;th&gt;Role&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Lake Formation&lt;/td&gt;
&lt;td&gt;Technical governance (who can access what)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Amazon DataZone&lt;/td&gt;
&lt;td&gt;Business governance (discovering, understanding, and requesting data)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;A useful way to think about them is:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Lake Formation = Technical foundation for governance&lt;/li&gt;
&lt;li&gt;DataZone = Business foundation for governance&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Combined with Glue Data Catalog, these services form a comprehensive data catalog and governance solution on AWS.&lt;/p&gt;




&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;In this article, I reviewed AWS Lake Formation from its fundamentals through practical implementation patterns.&lt;/p&gt;

&lt;p&gt;While there is a learning curve, it is an extremely important service for implementing proper data governance.&lt;/p&gt;

&lt;p&gt;Key takeaways:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Lake Formation complements IAM rather than replacing it, adding fine-grained table-, column-, and row-level controls.&lt;/li&gt;
&lt;li&gt;Column, row, and cell-level security are implemented through Data Filters.&lt;/li&gt;
&lt;li&gt;LF-TBAC reduces operational overhead as the number of tables grows.&lt;/li&gt;
&lt;li&gt;Lake Formation integrates tightly with Glue Data Catalog by adding a governance layer on top of metadata management.&lt;/li&gt;
&lt;li&gt;Understanding IAMAllowedPrincipals and using Hybrid Access Mode for gradual adoption is essential in existing environments.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Lake Formation certainly introduces some complexity, but when implementing proper access control in a data lake, the limitations of IAM alone eventually become apparent.&lt;/p&gt;

&lt;p&gt;In environments where data is consumed by multiple teams and a wide variety of users, Lake Formation is well worth considering.&lt;/p&gt;

&lt;p&gt;That said, successful adoption depends on maintaining consistency across IAM, Lake Formation, and S3 bucket policies, so careful planning is essential.&lt;/p&gt;

&lt;p&gt;I hope this article helps anyone considering the adoption of Lake Formation.&lt;/p&gt;

</description>
      <category>aws</category>
      <category>dataengineering</category>
    </item>
    <item>
      <title>Rethinking Lakehouse Architecture Through Data Ownership: AWS vs. Snowflake</title>
      <dc:creator>Aki</dc:creator>
      <pubDate>Mon, 01 Jun 2026 13:31:04 +0000</pubDate>
      <link>https://dev.to/aws-builders/rethinking-lakehouse-architecture-through-data-ownership-aws-vs-snowflake-336e</link>
      <guid>https://dev.to/aws-builders/rethinking-lakehouse-architecture-through-data-ownership-aws-vs-snowflake-336e</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Original Japanese article&lt;/strong&gt;: &lt;a href="https://zenn.dev/penginpenguin/articles/e2662e118ef61d" rel="noopener noreferrer"&gt;データの主導権から考えるAWSとSnowflakeのレイクハウスアーキテクチャ&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Introduction
&lt;/h2&gt;

&lt;p&gt;I'm Aki, an AWS Community Builder (&lt;a href="https://x.com/jitepengin" rel="noopener noreferrer"&gt;@jitepengin&lt;/a&gt;).&lt;br&gt;
When designing a data platform, discussions about whether to lean toward AWS or Snowflake are still very common.&lt;/p&gt;

&lt;p&gt;However, with the rise of Apache Iceberg, data and platforms can now be decoupled. Because of this shift, I believe we need to reconsider the question itself.&lt;/p&gt;

&lt;p&gt;Rather than asking:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Should we build around AWS or Snowflake?&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;A more fundamental question is:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Who owns the data?&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;In this article, I'd like to define what I mean by &lt;em&gt;data ownership&lt;/em&gt; and explore the architectural trade-offs of AWS-centric and Snowflake-centric lakehouse designs.&lt;/p&gt;


&lt;h1&gt;
  
  
  Why Data Ownership Matters
&lt;/h1&gt;

&lt;p&gt;Apache Iceberg has made it possible to separate data from the platform that accesses it.&lt;/p&gt;

&lt;p&gt;Today, an Iceberg table stored on Amazon S3 can be accessed from Athena, Snowflake, Spark, and many other engines. As a result, choosing a product is becoming less important than deciding who is responsible for managing the data.&lt;/p&gt;

&lt;p&gt;Before diving into architectural patterns, let's first examine why this shift matters.&lt;/p&gt;
&lt;h2&gt;
  
  
  Defining Ownership Across Three Layers
&lt;/h2&gt;

&lt;p&gt;In this article, I define &lt;strong&gt;data ownership&lt;/strong&gt; through the following three layers:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Layer&lt;/th&gt;
&lt;th&gt;Question&lt;/th&gt;
&lt;th&gt;Example&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Catalog Ownership&lt;/td&gt;
&lt;td&gt;Who owns the metadata?&lt;/td&gt;
&lt;td&gt;Glue Data Catalog / Snowflake Open Catalog&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Write Ownership&lt;/td&gt;
&lt;td&gt;Who can update or delete data?&lt;/td&gt;
&lt;td&gt;Glue ETL / Snowflake DML&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Governance Ownership&lt;/td&gt;
&lt;td&gt;Who controls access policies?&lt;/td&gt;
&lt;td&gt;Lake Formation / Snowflake Horizon&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Only when these three layers are consistently controlled by the same authority can we truly say that ownership exists.&lt;/p&gt;

&lt;p&gt;Conversely, when ownership is distributed or unclear, complexity tends to emerge in architecture, operations, and security.&lt;/p&gt;


&lt;h2&gt;
  
  
  The Reality of Vendor Lock-In
&lt;/h2&gt;

&lt;p&gt;Even in the Iceberg era, platform dependencies have not disappeared—they have simply changed form.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Catalog dependency&lt;/strong&gt;: Tables managed by Snowflake Open Catalog still rely operationally on a Snowflake-managed service, although external engines can access them through the REST Catalog API.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Write-engine dependency&lt;/strong&gt;: Snowflake-managed Iceberg tables are primarily updated through Snowflake, though Horizon Catalog now supports external writes from engines such as Spark. The choice of write engine remains closely tied to catalog design.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Governance dependency&lt;/strong&gt;: Lake Formation's fine-grained permissions are fundamentally tied to the AWS ecosystem.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Therefore, saying that "Iceberg eliminates vendor lock-in" is only partially true.&lt;/p&gt;

&lt;p&gt;What Iceberg removes is &lt;strong&gt;storage-format lock-in&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Dependencies around catalog management, governance, and operational processes still remain. In practice, migrating a data platform involves challenges such as governance policies, access control, metadata management, and platform-specific features.&lt;/p&gt;


&lt;h2&gt;
  
  
  Extensibility and Strategic Flexibility
&lt;/h2&gt;

&lt;p&gt;Data platforms are never finished.&lt;/p&gt;

&lt;p&gt;The rapid evolution of AI technologies and the continuous changes in the Modern Data Stack mean that architectures must adapt over time.&lt;/p&gt;

&lt;p&gt;Common examples include:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Adding or changing analytics tools&lt;/strong&gt;&lt;br&gt;
Athena may be sufficient initially, but business users may later request Snowflake access.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Introducing AI workloads&lt;/strong&gt;&lt;br&gt;
Integration with SageMaker or Snowflake Cortex AI may become necessary.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Cost optimization initiatives&lt;/strong&gt;&lt;br&gt;
As query volumes grow, Snowflake compute costs may become significant, leading teams to move batch processing to EMR.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Stronger governance requirements&lt;/strong&gt;&lt;br&gt;
Column masking or row-level security may need to be introduced later.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;When ownership across the three layers is clearly defined from the beginning, these changes become easier to evaluate and implement.&lt;/p&gt;

&lt;p&gt;Without that clarity, every change raises new questions about where responsibilities and controls should reside.&lt;/p&gt;


&lt;h1&gt;
  
  
  What Changed After Iceberg?
&lt;/h1&gt;

&lt;p&gt;Historically, data and platforms were tightly coupled.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;Snowflake-Centric&lt;/th&gt;
&lt;th&gt;AWS-Centric&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Data Location&lt;/td&gt;
&lt;td&gt;Inside Snowflake&lt;/td&gt;
&lt;td&gt;Inside S3&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Management Ownership&lt;/td&gt;
&lt;td&gt;Snowflake owns everything&lt;/td&gt;
&lt;td&gt;AWS owns everything&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Access from Other Engines&lt;/td&gt;
&lt;td&gt;Not possible&lt;/td&gt;
&lt;td&gt;Snowflake could not access directly&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Iceberg fundamentally changed this model.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Iceberg Tables on S3
        ↓
Shared by Multiple Engines

Athena / Glue / Snowflake / Spark / Redshift ...
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Iceberg adds a metadata layer on top of Parquet files stored in object storage, enabling ACID transactions and schema evolution independent of any specific compute engine.&lt;/p&gt;

&lt;p&gt;A catalog tracks metadata such as schemas and active data files, allowing multiple engines to safely access the same table.&lt;/p&gt;

&lt;p&gt;Data files are now shareable.&lt;/p&gt;

&lt;p&gt;However, ownership of the catalog, write operations, and governance still depends on architectural decisions.&lt;/p&gt;

&lt;p&gt;In other words, deciding who manages the catalog effectively determines who owns the data.&lt;/p&gt;




&lt;blockquote&gt;
&lt;h3&gt;
  
  
  Major Iceberg Catalog Options
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Catalog&lt;/th&gt;
&lt;th&gt;Type&lt;/th&gt;
&lt;th&gt;Characteristics&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;AWS Glue Data Catalog&lt;/td&gt;
&lt;td&gt;AWS-managed&lt;/td&gt;
&lt;td&gt;Supports REST Catalog API and integrates with Lake Formation governance&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Snowflake Open Catalog&lt;/td&gt;
&lt;td&gt;Snowflake-managed (based on Apache Polaris)&lt;/td&gt;
&lt;td&gt;REST Catalog compliant and accessible from Spark, Trino, and others&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Snowflake Horizon Catalog&lt;/td&gt;
&lt;td&gt;Snowflake service&lt;/td&gt;
&lt;td&gt;Exposes Snowflake-managed Iceberg tables through APIs; differs from Open Catalog because it is not a standalone metadata store&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;
&lt;/blockquote&gt;




&lt;h1&gt;
  
  
  Snowflake-Centric Architecture
&lt;/h1&gt;

&lt;h2&gt;
  
  
  Characteristics
&lt;/h2&gt;

&lt;p&gt;In this approach, Snowflake becomes the center of catalog management, governance, and analytics, while data files remain in external object storage such as S3.&lt;/p&gt;

&lt;p&gt;This model prioritizes simplicity and a streamlined analytics experience.&lt;/p&gt;

&lt;h2&gt;
  
  
  Ownership Model
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Layer&lt;/th&gt;
&lt;th&gt;Owner&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Catalog Ownership&lt;/td&gt;
&lt;td&gt;Snowflake Open Catalog or Horizon Catalog&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Write Ownership&lt;/td&gt;
&lt;td&gt;Primarily Snowflake DML&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Governance Ownership&lt;/td&gt;
&lt;td&gt;Snowflake Horizon&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Although data files remain on S3, external engines can access Snowflake-managed Iceberg tables through two mechanisms:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Via Open Catalog&lt;/strong&gt;: Snowflake-managed Iceberg tables are synced to Open Catalog and exposed through the REST Catalog API. In this sync scenario, external engines have read-only access. (Note: when Open Catalog itself is used as an internal catalog, read/write access is supported.)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Via Horizon Catalog&lt;/strong&gt;: Tables are exposed directly through the Horizon Iceberg REST Catalog API without syncing to Open Catalog. External engines can both read and write, and existing Snowflake users and roles can be used for access control.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Benefits
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Governance policies such as column masking and row-level security can be applied to Iceberg tables in the same way as native Snowflake tables. When external engines access tables through Horizon Catalog, the same policies are enforced at read time. Note, however, that &lt;strong&gt;writing to tables with masking policies or tags applied is not supported from external engines&lt;/strong&gt; — this is an important constraint to be aware of.&lt;/li&gt;
&lt;li&gt;Rich ecosystem support for BI tools such as Power BI makes Snowflake a convenient analytics front end.&lt;/li&gt;
&lt;li&gt;External engines can access Iceberg tables through Open Catalog or Horizon Catalog while reusing Snowflake users and roles as the unit of access control.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Drawbacks
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Snowflake warehouse compute costs can be significant for write-heavy workloads. When external engines such as Spark write through Horizon Catalog, Snowflake warehouses are not used — but &lt;strong&gt;Horizon Catalog API calls are billed at 0.5 credits per million requests&lt;/strong&gt;, so cost planning is still required.&lt;/li&gt;
&lt;li&gt;Coordination is needed when AWS services such as Glue ETL also write to the same datasets. Clearly defining who holds catalog ownership is essential.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Even with Iceberg, many enterprises ultimately converge on a Snowflake-centric operating model because governance, metadata, and write operations all remain concentrated within Snowflake.&lt;/p&gt;

&lt;p&gt;In such cases, Iceberg provides openness in theory, but ownership remains firmly within the Snowflake ecosystem.&lt;/p&gt;




&lt;h1&gt;
  
  
  AWS-Centric Architecture
&lt;/h1&gt;

&lt;h2&gt;
  
  
  Characteristics
&lt;/h2&gt;

&lt;p&gt;This architecture uses S3 for storage, Glue Data Catalog for metadata, and AWS-native services for ETL, analytics, and governance.&lt;/p&gt;

&lt;p&gt;Its primary advantages are flexibility and service interoperability.&lt;/p&gt;

&lt;h2&gt;
  
  
  Ownership Model
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Layer&lt;/th&gt;
&lt;th&gt;Owner&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Catalog Ownership&lt;/td&gt;
&lt;td&gt;AWS Glue Data Catalog&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Write Ownership&lt;/td&gt;
&lt;td&gt;Glue ETL / EMR&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Governance Ownership&lt;/td&gt;
&lt;td&gt;Lake Formation&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Because Glue Data Catalog supports the Iceberg REST Catalog API, external engines such as Snowflake and Databricks can access the same tables.&lt;/p&gt;

&lt;p&gt;This enables AWS to retain ownership while allowing Snowflake to serve as an analytics front end.&lt;/p&gt;

&lt;h2&gt;
  
  
  Benefits
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Tight integration across Athena, Glue, EMR, and Redshift with a shared catalog.&lt;/li&gt;
&lt;li&gt;Fine-grained column- and row-level governance through Lake Formation, applicable to Iceberg tables.&lt;/li&gt;
&lt;li&gt;Ability to optimize compute engines for different workloads — EMR for large-scale batch, Athena for interactive queries.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Drawbacks
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Increased architectural and operational complexity due to the number of AWS services involved.&lt;/li&gt;
&lt;li&gt;Additional design considerations for multi-cloud environments, as the catalog remains AWS-dependent.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Lake Formation is powerful, but troubleshooting permission issues can become challenging. Identifying why a specific user cannot access a specific table or row often takes considerable time, requiring mature operational practices and careful permission design.&lt;/p&gt;




&lt;h1&gt;
  
  
  Combining AWS and Snowflake
&lt;/h1&gt;

&lt;p&gt;A realistic approach is not choosing one platform over the other, but assigning clear responsibilities to each.&lt;/p&gt;

&lt;p&gt;The key is defining ownership boundaries upfront.&lt;/p&gt;

&lt;h2&gt;
  
  
  AWS Owns the Data, Snowflake Powers Analytics
&lt;/h2&gt;

&lt;p&gt;This is one of the most common patterns.&lt;/p&gt;

&lt;p&gt;The goal is to maintain data ownership within AWS while leveraging Snowflake's analytics capabilities and its rich ecosystem of BI connectors.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;┌──────────────────────────────────────────────────┐
│                       AWS                        │
│  S3 (Iceberg data files)                         │
│  Glue Data Catalog (Catalog Ownership)           │
│  Lake Formation (Governance Ownership)           │
│  Glue / EMR (Write Ownership)                    │
└──────────────────────┬───────────────────────────┘
                       │ Iceberg REST Catalog API
        ┌──────────────┼───────────────────┐
        ▼              ▼                   ▼
     Athena          Glue              Snowflake
  (Interactive)     (ETL)            (Analytics)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In this model:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Layer&lt;/th&gt;
&lt;th&gt;Owner&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Catalog Ownership&lt;/td&gt;
&lt;td&gt;AWS&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Write Ownership&lt;/td&gt;
&lt;td&gt;AWS&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Governance Ownership&lt;/td&gt;
&lt;td&gt;AWS&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Snowflake acts primarily as an analytical interface.&lt;/p&gt;

&lt;p&gt;Two variations exist:&lt;/p&gt;

&lt;h3&gt;
  
  
  1. Glue Catalog Integration (Read-Only)
&lt;/h3&gt;

&lt;p&gt;Snowflake accesses AWS-managed Iceberg tables through External Iceberg Tables. Write ownership and governance remain entirely with AWS. Lake Formation can be used as the single source of truth for access control.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Catalog-Linked Database (Read/Write)
&lt;/h3&gt;

&lt;p&gt;Snowflake can update Iceberg tables through the Iceberg REST Catalog API while the data remains stored on S3. This approach is attractive when analysts and AI workloads primarily operate in Snowflake.&lt;/p&gt;

&lt;p&gt;However, governance responsibilities become shared between AWS and Snowflake. Both Lake Formation and Snowflake-side access controls must be configured carefully — a misconfiguration in either can become a security gap. If the read-only pattern (option 1) is sufficient, consolidating governance in Lake Formation is simpler.&lt;/p&gt;

&lt;p&gt;For step-by-step implementation details of these patterns — including how to set up External Volumes, Catalog Integrations, and Catalog-Linked Databases — see this companion article:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://dev.to/aws-builders/aws-snowflake-lakehouse-2-practical-apache-iceberg-integration-patterns-812"&gt;AWS Snowflake Lakehouse: 2 Practical Apache Iceberg Integration Patterns&lt;/a&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  Comparison: Three Architectural Patterns
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Dimension&lt;/th&gt;
&lt;th&gt;Snowflake-Centric&lt;/th&gt;
&lt;th&gt;AWS-Centric&lt;/th&gt;
&lt;th&gt;Hybrid&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Catalog Ownership&lt;/td&gt;
&lt;td&gt;Snowflake&lt;/td&gt;
&lt;td&gt;AWS&lt;/td&gt;
&lt;td&gt;AWS&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Write Ownership&lt;/td&gt;
&lt;td&gt;Snowflake&lt;/td&gt;
&lt;td&gt;AWS&lt;/td&gt;
&lt;td&gt;AWS&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Governance Ownership&lt;/td&gt;
&lt;td&gt;Snowflake Horizon&lt;/td&gt;
&lt;td&gt;Lake Formation&lt;/td&gt;
&lt;td&gt;AWS primary (①) / AWS+SF (②)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Compute Cost&lt;/td&gt;
&lt;td&gt;Tends to be higher&lt;/td&gt;
&lt;td&gt;Optimizable by workload&lt;/td&gt;
&lt;td&gt;Optimizable by workload&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Operational Complexity&lt;/td&gt;
&lt;td&gt;Low to medium&lt;/td&gt;
&lt;td&gt;Medium to high&lt;/td&gt;
&lt;td&gt;High&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Multi-Engine Flexibility&lt;/td&gt;
&lt;td&gt;Medium (via REST API)&lt;/td&gt;
&lt;td&gt;High&lt;/td&gt;
&lt;td&gt;High&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  Choosing the Right Pattern
&lt;/h2&gt;

&lt;p&gt;Based on the patterns above, here is a simplified decision guide:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Snowflake-centric tends to fit when:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Analytics is BI-driven or led by non-engineers&lt;/li&gt;
&lt;li&gt;Development speed and analytics experience take priority over data volume&lt;/li&gt;
&lt;li&gt;Centralized governance through Snowflake Horizon is preferred&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;AWS-centric tends to fit when:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Data volumes are large and ETL is the dominant workload&lt;/li&gt;
&lt;li&gt;A dedicated data engineering team is already working within the AWS ecosystem&lt;/li&gt;
&lt;li&gt;Fine-grained access control through Lake Formation is a requirement&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Hybrid tends to fit when:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Different teams use different tools (e.g., engineers on AWS, analysts on Snowflake)&lt;/li&gt;
&lt;li&gt;Future extensibility for AI, ML, or multi-engine workloads is a priority&lt;/li&gt;
&lt;li&gt;AWS retains data ownership while Snowflake's query performance is still needed&lt;/li&gt;
&lt;/ul&gt;




&lt;h1&gt;
  
  
  What Happens When Ownership Is Unclear
&lt;/h1&gt;

&lt;p&gt;A common anti-pattern is building a platform that "works" without explicitly defining ownership.&lt;/p&gt;

&lt;p&gt;Typical symptoms include:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Nobody knows who is responsible for schema changes.&lt;/strong&gt; When both Glue and Snowflake have schema owners, it becomes unclear which definition is authoritative.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Data written from Snowflake is not visible in Athena.&lt;/strong&gt; When two catalogs attempt to manage the same table, one may lose track of the latest snapshot, causing metadata inconsistencies.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Governance rules drift between Lake Formation and Snowflake Horizon.&lt;/strong&gt; Maintaining access policies in two places creates risk — a gap in either becomes a security vulnerability.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Incident response slows down.&lt;/strong&gt; When multiple engines can write, identifying what happened and where becomes difficult, delaying recovery.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;These issues often evolve from technical challenges into organizational problems:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Teams blame each other over unclear responsibilities.&lt;/li&gt;
&lt;li&gt;Audits become difficult because nobody can fully explain who has access to what.&lt;/li&gt;
&lt;li&gt;Incident recovery is delayed due to unclear decision-making authority.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A running system is not necessarily a well-designed system.&lt;/p&gt;

&lt;p&gt;Ownership becomes increasingly difficult to fix after the platform has already grown.&lt;/p&gt;




&lt;h1&gt;
  
  
  "AWS or Snowflake?" Is a Secondary Question
&lt;/h1&gt;

&lt;p&gt;In practice, organizations often begin by debating whether to standardize on AWS or Snowflake.&lt;/p&gt;

&lt;p&gt;In the Iceberg era, I believe that is the wrong starting point.&lt;/p&gt;

&lt;p&gt;The first questions should be:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Who owns the catalog?&lt;/li&gt;
&lt;li&gt;Who owns writes?&lt;/li&gt;
&lt;li&gt;Who owns governance?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Once these three ownership layers are defined, the platform choice naturally follows.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Want all three owned by Snowflake? → Snowflake-centric architecture.&lt;/li&gt;
&lt;li&gt;Want all three owned by AWS? → AWS-centric architecture.&lt;/li&gt;
&lt;li&gt;Want AWS to own data while Snowflake provides analytics? → Hybrid architecture.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Iceberg has dramatically increased flexibility around where data lives.&lt;/p&gt;

&lt;p&gt;As flexibility increases, architects must become more deliberate about defining responsibility.&lt;/p&gt;

&lt;p&gt;Starting with product selection often leads to contradictions later. A configuration where Snowflake is used as the query interface, Glue handles writes, and Lake Formation controls governance — without intentional design — is a classic symptom of ownership being distributed and unclear from the start.&lt;/p&gt;

&lt;p&gt;The hardest challenge is no longer connectivity.&lt;/p&gt;

&lt;p&gt;It is ownership.&lt;/p&gt;




&lt;h1&gt;
  
  
  Conclusion
&lt;/h1&gt;

&lt;p&gt;Apache Iceberg has significantly reduced storage-level vendor lock-in.&lt;/p&gt;

&lt;p&gt;However, catalog ownership, write ownership, and governance ownership still require deliberate architectural decisions.&lt;/p&gt;

&lt;p&gt;A useful decision-making sequence is:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Decide who owns the catalog. (Glue / Snowflake Open Catalog / Snowflake Horizon)&lt;/li&gt;
&lt;li&gt;Decide who owns writes. (AWS-native services / Snowflake)&lt;/li&gt;
&lt;li&gt;Decide who owns governance. (Lake Formation / Snowflake Horizon / both)&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Once those three decisions are made, choosing between AWS and Snowflake becomes much easier. From there, you can design the architecture that best fits your requirements.&lt;/p&gt;

&lt;p&gt;Ultimately, the hardest part of a modern lakehouse architecture is often not the technology itself. It is agreeing on ownership boundaries — deciding which team manages the catalog, who is responsible for data updates, and where governance policies are enforced.&lt;/p&gt;

&lt;p&gt;Technology evolves. The challenge of people and processes remains.&lt;/p&gt;

&lt;p&gt;I hope this article helps anyone evaluating lakehouse architectures built on AWS, Snowflake, and Apache Iceberg.&lt;/p&gt;

</description>
      <category>aws</category>
      <category>snowflake</category>
      <category>iceberg</category>
      <category>dataengineering</category>
    </item>
    <item>
      <title>Exploring Snowpark While Comparing It with Apache Spark</title>
      <dc:creator>Aki</dc:creator>
      <pubDate>Mon, 01 Jun 2026 04:00:00 +0000</pubDate>
      <link>https://dev.to/datapenguin/exploring-snowpark-while-comparing-it-with-apache-spark-mki</link>
      <guid>https://dev.to/datapenguin/exploring-snowpark-while-comparing-it-with-apache-spark-mki</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Original Japanese article&lt;/strong&gt;: &lt;a href="https://zenn.dev/penginpenguin/articles/91be27b34c7309" rel="noopener noreferrer"&gt;Snowparkを動かしながらSparkとの違いを整理してみる&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Introduction
&lt;/h2&gt;

&lt;p&gt;Recently, I've had more opportunities to work with Snowflake when building data platforms.&lt;/p&gt;

&lt;p&gt;When working with modern data platforms, Apache Spark is often used for distributed data processing. Snowflake also provides its own data processing framework called Snowpark.&lt;/p&gt;

&lt;p&gt;If you're already familiar with Spark or AWS Glue, you may find yourself wondering:&lt;/p&gt;

&lt;p&gt;&lt;em&gt;"Wait... how is Snowpark actually different from Spark?"&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;In this article, I'd like to organize my own understanding while exploring Snowpark's behavior and comparing it with Spark.&lt;/p&gt;

&lt;p&gt;For this experiment, everything was done entirely within Snowflake Notebooks in Snowsight.&lt;/p&gt;

&lt;p&gt;One of the biggest advantages is that no local environment setup or connection configuration is required—you can start experimenting immediately.&lt;/p&gt;




&lt;h2&gt;
  
  
  What is Snowpark?
&lt;/h2&gt;

&lt;p&gt;Snowpark is a data processing framework provided by Snowflake.&lt;/p&gt;

&lt;p&gt;Its biggest feature is the ability to write code in Python, Java, or Scala and execute it directly inside Snowflake.&lt;/p&gt;

&lt;p&gt;Traditionally, Snowflake workloads were primarily implemented using SQL. With Snowpark, however, you can use a DataFrame API similar to Spark or Pandas while keeping all processing within Snowflake.&lt;/p&gt;

&lt;p&gt;In other words, you no longer need to pull data into a local environment or AWS Lambda for processing.&lt;/p&gt;

&lt;p&gt;Some key characteristics include:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Managed execution environment – Processing runs on Snowflake warehouses with no infrastructure management required.&lt;/li&gt;
&lt;li&gt;DataFrame API – Similar developer experience to Spark and Pandas.&lt;/li&gt;
&lt;li&gt;Pushdown execution – Code is executed within Snowflake, eliminating data transfer overhead.&lt;/li&gt;
&lt;li&gt;UDF and UDTF support – Custom functions can be defined and executed inside Snowflake.&lt;/li&gt;
&lt;li&gt;Snowflake Notebook integration – Interactive development is supported directly in Snowsight.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Personally, I still like Scala, but these days I write most data processing code in Python.&lt;/p&gt;

&lt;p&gt;While Scala often offers better performance, Python's simplicity and extensive ecosystem make it the more practical choice in many situations.&lt;/p&gt;




&lt;h2&gt;
  
  
  Differences Between Spark and Snowpark
&lt;/h2&gt;

&lt;p&gt;Many people immediately think of Spark when they hear the name Snowpark.&lt;/p&gt;

&lt;p&gt;The names are similar, and the DataFrame APIs feel very familiar.&lt;/p&gt;

&lt;p&gt;However, there are several important differences.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Aspect&lt;/th&gt;
&lt;th&gt;Spark&lt;/th&gt;
&lt;th&gt;Snowpark&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Execution Environment&lt;/td&gt;
&lt;td&gt;Distributed cluster&lt;/td&gt;
&lt;td&gt;Snowflake warehouse&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Data Source&lt;/td&gt;
&lt;td&gt;HDFS, S3, and others&lt;/td&gt;
&lt;td&gt;Primarily Snowflake tables&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Scaling&lt;/td&gt;
&lt;td&gt;Cluster size managed by user&lt;/td&gt;
&lt;td&gt;Warehouse size adjustment&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Languages&lt;/td&gt;
&lt;td&gt;Scala, Java, Python, R, etc.&lt;/td&gt;
&lt;td&gt;Python, Java, Scala&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;External Data Support&lt;/td&gt;
&lt;td&gt;Broad ecosystem support&lt;/td&gt;
&lt;td&gt;Primarily Snowflake-centric&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Infrastructure Management&lt;/td&gt;
&lt;td&gt;Cluster management required&lt;/td&gt;
&lt;td&gt;Fully managed&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Spark requires awareness of distributed clusters and execution mechanics.&lt;/p&gt;

&lt;p&gt;Snowpark, on the other hand, is fundamentally a processing framework that operates on top of the Snowflake platform.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;DataFrame operations are internally converted into SQL execution plans and executed by Snowflake's SQL engine.&lt;/p&gt;

&lt;p&gt;Unlike Spark, user code is not distributed across worker nodes.&lt;/p&gt;

&lt;p&gt;Scaling is handled by Snowflake warehouses.&lt;/p&gt;

&lt;p&gt;UDFs are an exception. UDF code is pushed into Snowflake and executed in parallel by Snowflake's infrastructure.&lt;/p&gt;

&lt;p&gt;A useful mental model is:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;DataFrame operations → SQL generation&lt;/li&gt;
&lt;li&gt;UDFs → Server-side parallel execution&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;In either case, users do not need to manage clusters or DAG execution as they would in Spark.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;If your data is already centralized in Snowflake, Snowpark provides a convenient way to write Spark-like code without worrying about infrastructure management.&lt;/p&gt;

&lt;p&gt;Of course, AWS Glue also provides a largely serverless experience, making it another convenient option in the AWS ecosystem.&lt;/p&gt;




&lt;h1&gt;
  
  
  Getting Started
&lt;/h1&gt;

&lt;p&gt;All examples in this article are executed within Snowflake Notebooks in Snowsight.&lt;/p&gt;

&lt;p&gt;No local Python environment or connection configuration is required—the entire workflow runs directly in the browser.&lt;/p&gt;

&lt;h2&gt;
  
  
  Prerequisites
&lt;/h2&gt;

&lt;p&gt;From the Snowsight menu:&lt;/p&gt;

&lt;p&gt;Create → Notebooks&lt;/p&gt;

&lt;p&gt;Create a new notebook.&lt;/p&gt;

&lt;p&gt;Snowpark for Python is already installed, so there is no need to run &lt;code&gt;pip install&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;You can start coding immediately.&lt;/p&gt;

&lt;h2&gt;
  
  
  Obtaining a Session
&lt;/h2&gt;

&lt;p&gt;In local environments, Snowpark sessions are typically created using:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;Session&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;builder&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;configs&lt;/span&gt;&lt;span class="p"&gt;(...).&lt;/span&gt;&lt;span class="nf"&gt;create&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In Snowflake Notebooks, an active session already exists.&lt;/p&gt;

&lt;p&gt;You can simply retrieve it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;snowflake.snowpark.context&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;get_active_session&lt;/span&gt;

&lt;span class="n"&gt;session&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;get_active_session&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Session acquired successfully!&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Flu0m5qkzeq2df5t9spvr.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Flu0m5qkzeq2df5t9spvr.png" width="799" height="323"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;One of the major advantages of Snowflake Notebooks is that connection details never need to be written manually.&lt;/p&gt;




&lt;h2&gt;
  
  
  Basic DataFrame Operations
&lt;/h2&gt;

&lt;p&gt;Let's create and manipulate a DataFrame from a table.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;df&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;session&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;table&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;MY_DB.MY_SCHEMA.SALES_DATA&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="n"&gt;result_df&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;df&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;select&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;ORDER_ID&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;AMOUNT&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;REGION&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; \
              &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;filter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;df&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;REGION&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Asia&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; \
              &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sort&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;df&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;AMOUNT&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nf"&gt;desc&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;

&lt;span class="n"&gt;result_df&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;show&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fcqsy6sufuherfu9gmexo.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fcqsy6sufuherfu9gmexo.png" width="800" height="486"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;One important point is that no SQL is actually executed until &lt;code&gt;show()&lt;/code&gt; is called.&lt;/p&gt;

&lt;p&gt;We'll discuss this in more detail when covering lazy evaluation.&lt;/p&gt;




&lt;h2&gt;
  
  
  Aggregations
&lt;/h2&gt;

&lt;p&gt;GroupBy operations feel almost identical to Spark.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;snowflake.snowpark&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;functions&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;F&lt;/span&gt;

&lt;span class="n"&gt;summary_df&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;df&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;group_by&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;REGION&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; \
               &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;agg&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
                   &lt;span class="n"&gt;F&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sum&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;AMOUNT&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;alias&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;TOTAL_AMOUNT&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
                   &lt;span class="n"&gt;F&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;count&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;ORDER_ID&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;alias&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;ORDER_COUNT&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
                   &lt;span class="n"&gt;F&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;avg&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;AMOUNT&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;alias&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;AVG_AMOUNT&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
               &lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="n"&gt;summary_df&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;show&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fogu4lrrn4ep4asbl1tpf.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fogu4lrrn4ep4asbl1tpf.png" width="800" height="511"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;When executed, these DataFrame operations are translated into SQL and run within Snowflake.&lt;/p&gt;

&lt;p&gt;You can inspect the generated SQL using:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;summary_df&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;queries&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F6a6kpk8p5t5vw8xt39os.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F6a6kpk8p5t5vw8xt39os.png" width="799" height="298"&gt;&lt;/a&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  Writing Results Back to a Table
&lt;/h2&gt;

&lt;p&gt;To save results into a Snowflake table:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;summary_df&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;write&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;mode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;overwrite&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;save_as_table&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;MY_DB.MY_SCHEMA.SALES_SUMMARY&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Since &lt;code&gt;save_as_table()&lt;/code&gt; does not return a result, it's often useful to reload the table to verify the output.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;session&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;table&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;MY_DB.MY_SCHEMA.SALES_SUMMARY&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;show&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fm0szjnnu89853uqs3dfs.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fm0szjnnu89853uqs3dfs.png" width="800" height="441"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;overwrite&lt;/code&gt; mode replaces the existing table.&lt;/p&gt;

&lt;p&gt;Use &lt;code&gt;append&lt;/code&gt; if you want to add rows instead.&lt;/p&gt;




&lt;h1&gt;
  
  
  How Does Lazy Evaluation Work?
&lt;/h1&gt;

&lt;p&gt;Anyone familiar with Spark has likely encountered lazy evaluation.&lt;/p&gt;

&lt;p&gt;Sometimes it can even lead to unexpected behavior during debugging.&lt;/p&gt;

&lt;p&gt;Snowpark adopts the same fundamental concept.&lt;/p&gt;

&lt;h2&gt;
  
  
  Understanding Lazy Evaluation
&lt;/h2&gt;

&lt;p&gt;DataFrame transformations such as:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;code&gt;select&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;filter&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;group_by&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;are not executed immediately.&lt;/p&gt;

&lt;p&gt;These operations merely build an execution plan.&lt;/p&gt;

&lt;p&gt;Actual execution occurs only when an action is triggered.&lt;/p&gt;

&lt;p&gt;Common action operations include:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;code&gt;show()&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;collect()&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;count()&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;to_pandas()&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;write.save_as_table()&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Verifying Lazy Evaluation
&lt;/h2&gt;

&lt;p&gt;A convenient way to inspect behavior is through &lt;code&gt;df.queries&lt;/code&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;snowflake.snowpark&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;functions&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;F&lt;/span&gt;

&lt;span class="n"&gt;df_filtered&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;session&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;table&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;MY_DB.MY_SCHEMA.LARGE_TABLE&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; \
                     &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;filter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;F&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;col&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;STATUS&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;ACTIVE&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; \
                     &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;select&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;ID&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;NAME&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;STATUS&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;CREATED_AT&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;df_filtered&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;queries&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="n"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;df_filtered&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;collect&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; rows retrieved&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The generated SQL can be inspected before execution, but no query has actually been sent to Snowflake yet.&lt;/p&gt;

&lt;p&gt;To verify execution timing precisely, we can use Query History in Snowsight.&lt;/p&gt;

&lt;p&gt;Open:&lt;/p&gt;

&lt;p&gt;Monitoring → Query History&lt;/p&gt;

&lt;p&gt;Then perform the following steps:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Define the DataFrame.&lt;/li&gt;
&lt;li&gt;Check Query History.&lt;/li&gt;
&lt;li&gt;Confirm that no SELECT statement has been executed.&lt;/li&gt;
&lt;li&gt;Execute &lt;code&gt;collect()&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Refresh Query History.&lt;/li&gt;
&lt;li&gt;Observe that the SELECT statement now appears.&lt;/li&gt;
&lt;/ol&gt;

&lt;h3&gt;
  
  
  Define the DataFrame
&lt;/h3&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fo6igp01ad2xq52zv19bd.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fo6igp01ad2xq52zv19bd.png" width="800" height="404"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;No corresponding query appears yet.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Flingdk3583mtx7yfqtda.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Flingdk3583mtx7yfqtda.png" width="800" height="288"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Execute collect()
&lt;/h3&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F1r0sl0nylkrbmpl76tvb.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F1r0sl0nylkrbmpl76tvb.png" width="800" height="447"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;After execution, the query becomes visible.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fx8syemsxtaec8882ooi1.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fx8syemsxtaec8882ooi1.png" width="800" height="351"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;This confirms that DataFrame definitions alone do not trigger execution.&lt;/p&gt;

&lt;p&gt;The SQL is executed only when &lt;code&gt;collect()&lt;/code&gt; is called.&lt;/p&gt;




&lt;h2&gt;
  
  
  Differences from Spark's Lazy Evaluation
&lt;/h2&gt;

&lt;p&gt;In Spark, lazy evaluation constructs a DAG that is optimized and executed across a cluster.&lt;/p&gt;

&lt;p&gt;In Snowpark, lazy evaluation ultimately produces SQL, which is then optimized and executed by Snowflake's query optimizer.&lt;/p&gt;

&lt;p&gt;The concept is similar, but the execution engine is fundamentally different.&lt;/p&gt;

&lt;p&gt;One particularly useful feature is that generated SQL can be inspected via &lt;code&gt;df.queries&lt;/code&gt;, making it easier to validate execution plans.&lt;/p&gt;




&lt;h1&gt;
  
  
  Can We Use Caching?
&lt;/h1&gt;

&lt;p&gt;If you're coming from Spark, your first instinct may be to use &lt;code&gt;cache()&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Snowpark provides a similar capability through &lt;code&gt;cache_result()&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Differences from Spark cache()
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Aspect&lt;/th&gt;
&lt;th&gt;Spark &lt;code&gt;cache()&lt;/code&gt;
&lt;/th&gt;
&lt;th&gt;Snowpark &lt;code&gt;cache_result()&lt;/code&gt;
&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Storage&lt;/td&gt;
&lt;td&gt;Memory (and disk)&lt;/td&gt;
&lt;td&gt;Temporary table&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Lifetime&lt;/td&gt;
&lt;td&gt;Until application ends&lt;/td&gt;
&lt;td&gt;Until session ends&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Cost&lt;/td&gt;
&lt;td&gt;No additional write&lt;/td&gt;
&lt;td&gt;INSERT into temporary table&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Internally, &lt;code&gt;cache_result()&lt;/code&gt; materializes results into a temporary table.&lt;/p&gt;

&lt;p&gt;Subsequent operations reuse that table rather than re-running expensive transformations.&lt;/p&gt;

&lt;h2&gt;
  
  
  Example
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;df_heavy&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;session&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;table&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;MY_DB.MY_SCHEMA.LARGE_TABLE&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; \
                  &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;filter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;F&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;col&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;STATUS&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;ACTIVE&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; \
                  &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
                      &lt;span class="n"&gt;session&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;table&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;MY_DB.MY_SCHEMA.MASTER&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
                      &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;ID&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
                  &lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="n"&gt;cached_df&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;df_heavy&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;cache_result&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

&lt;span class="n"&gt;result1&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;cached_df&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;filter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;F&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;col&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;REGION&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Asia&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;collect&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

&lt;span class="n"&gt;result2&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;cached_df&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;group_by&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;REGION&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; \
                   &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;agg&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;F&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;count&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;*&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; \
                   &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;collect&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

&lt;span class="n"&gt;cached_df&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;drop_table&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Using a &lt;code&gt;with&lt;/code&gt; block is often more convenient because the temporary table is automatically dropped when the block exits.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="n"&gt;df_heavy&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;cache_result&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;cached_df&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;result1&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;cached_df&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;filter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;F&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;col&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;REGION&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Asia&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;collect&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

    &lt;span class="n"&gt;result2&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;cached_df&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;group_by&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;REGION&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; \
                       &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;agg&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;F&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;count&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;*&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; \
                       &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;collect&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;blockquote&gt;
&lt;p&gt;Since &lt;code&gt;cache_result()&lt;/code&gt; performs an INSERT into a temporary table, it can actually make things slower when the DataFrame is only used once.&lt;/p&gt;

&lt;p&gt;It's most effective when the same expensive transformation is reused multiple times.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;You can also observe this behavior in Snowsight.&lt;/p&gt;

&lt;p&gt;Temporary table creation:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fkyso0e39p4pdwq581b59.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fkyso0e39p4pdwq581b59.png" width="799" height="198"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Subsequent SELECT from the temporary table:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fap1ipg845lbgdxb1mhqw.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fap1ipg845lbgdxb1mhqw.png" width="800" height="382"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Another query reusing the same temporary table:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fkrx4pslv6cm53se94fek.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fkrx4pslv6cm53se94fek.png" width="800" height="278"&gt;&lt;/a&gt;&lt;/p&gt;




&lt;h1&gt;
  
  
  Use Cases
&lt;/h1&gt;

&lt;p&gt;Let's consider some practical scenarios where Snowpark can be useful.&lt;/p&gt;

&lt;h2&gt;
  
  
  ETL Pipelines
&lt;/h2&gt;

&lt;p&gt;Traditionally, pipelines often look like:&lt;/p&gt;

&lt;p&gt;S3 → Glue → Redshift&lt;/p&gt;

&lt;p&gt;With Snowpark, many transformations can be performed entirely within Snowflake.&lt;/p&gt;

&lt;p&gt;This reduces data movement and simplifies overall architecture.&lt;/p&gt;

&lt;p&gt;Example:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;raw_df&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;session&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;table&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;MY_DB.MY_SCHEMA.RAW_EVENTS&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="n"&gt;cleaned_df&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;raw_df&lt;/span&gt; \
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;filter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;raw_df&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;EVENT_TYPE&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;test&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; \
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;with_column&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;EVENT_DATE&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;F&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;to_date&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;raw_df&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;EVENT_TIMESTAMP&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt; \
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;drop_duplicates&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;USER_ID&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;EVENT_DATE&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;EVENT_TYPE&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="p"&gt;])&lt;/span&gt;

&lt;span class="n"&gt;aggregated_df&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;cleaned_df&lt;/span&gt; \
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;group_by&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;EVENT_DATE&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;EVENT_TYPE&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; \
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;agg&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;F&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;count&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;USER_ID&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
         &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;alias&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;USER_COUNT&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="n"&gt;aggregated_df&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;write&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;mode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;overwrite&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; \
             &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;save_as_table&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
                 &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;MY_DB.MY_SCHEMA.DAILY_EVENT_SUMMARY&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
             &lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="n"&gt;session&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;table&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;MY_DB.MY_SCHEMA.DAILY_EVENT_SUMMARY&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;show&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fbus1sf8ispbcvz0j4h5j.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fbus1sf8ispbcvz0j4h5j.png" width="800" height="499"&gt;&lt;/a&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  Custom Transformations Using UDFs
&lt;/h2&gt;

&lt;p&gt;Snowpark UDFs allow complex logic that would be cumbersome in SQL to be implemented in Python.&lt;/p&gt;

&lt;p&gt;You can register UDFs using either the &lt;code&gt;@udf&lt;/code&gt; decorator or &lt;code&gt;session.udf.register()&lt;/code&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;snowflake.snowpark.functions&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;udf&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;snowflake.snowpark.types&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;StringType&lt;/span&gt;

&lt;span class="nd"&gt;@udf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;return_type&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nc"&gt;StringType&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
     &lt;span class="n"&gt;input_types&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nc"&gt;StringType&lt;/span&gt;&lt;span class="p"&gt;()])&lt;/span&gt;
&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;normalize_region&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;region&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;region_map&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;US&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;North America&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;JP&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Asia&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;DE&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Europe&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;region_map&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;region&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Other&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="n"&gt;df&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;session&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;table&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;MY_DB.MY_SCHEMA.RAW_EVENTS&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="n"&gt;df_with_region&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;df&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;with_column&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;NORMALIZED_REGION&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nf"&gt;normalize_region&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;df&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;REGION_CODE&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If type hints are available, explicit type definitions can be omitted:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;snowflake.snowpark.functions&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;udf&lt;/span&gt;

&lt;span class="nd"&gt;@udf&lt;/span&gt;
&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;normalize_region&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;region&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;region_map&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;US&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;North America&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;JP&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Asia&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;DE&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Europe&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;region_map&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;region&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Other&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That said, simple transformations are often faster when implemented using built-in SQL functions.&lt;/p&gt;

&lt;p&gt;As always, benchmark before deciding.&lt;/p&gt;




&lt;h2&gt;
  
  
  Data Quality Validation
&lt;/h2&gt;

&lt;p&gt;Snowpark can also be used for data quality checks before processing continues.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;total_count&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;df&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;count&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="n"&gt;null_count&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;df&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;filter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;F&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;col&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;AMOUNT&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;is_null&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;count&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

&lt;span class="n"&gt;null_rate&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;null_count&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="n"&gt;total_count&lt;/span&gt;

&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;null_rate&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mf"&gt;0.10&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="nc"&gt;ValueError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;NULL rate exceeds the threshold: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;null_rate&lt;/span&gt;&lt;span class="si"&gt;:&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="o"&gt;%&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Data quality check passed &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;(NULL rate: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;null_rate&lt;/span&gt;&lt;span class="si"&gt;:&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="o"&gt;%&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;)&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fspfqmgi9wjotq1c6e2dv.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fspfqmgi9wjotq1c6e2dv.png" width="800" height="475"&gt;&lt;/a&gt;&lt;/p&gt;




&lt;h1&gt;
  
  
  Conclusion
&lt;/h1&gt;

&lt;p&gt;In this article, we explored Snowpark's fundamentals, compared it with Spark, and examined its lazy evaluation behavior.&lt;/p&gt;

&lt;p&gt;For engineers already familiar with Spark, Snowpark should feel quite approachable.&lt;/p&gt;

&lt;p&gt;However, it's important to remember that execution occurs on Snowflake warehouses rather than a Spark cluster.&lt;/p&gt;

&lt;p&gt;Reviewing generated SQL and understanding how Snowflake executes queries can help avoid unexpected full-table scans and other performance issues.&lt;/p&gt;

&lt;p&gt;If your data is already centralized in Snowflake, keeping processing inside Snowflake rather than moving data to Lambda or Glue Python Shell can be a significant advantage.&lt;/p&gt;

&lt;p&gt;Reducing infrastructure management overhead and consolidating ETL processing within Snowflake can also improve maintainability.&lt;/p&gt;

&lt;p&gt;One final note: throughout this experiment, I frequently relied on Cortex Code whenever I encountered errors.&lt;/p&gt;

&lt;p&gt;The workflow of iteratively fixing notebook errors through Cortex Code was surprisingly convenient.&lt;/p&gt;

&lt;p&gt;That said, just like any AI-assisted coding workflow, it's still important to carefully validate the generated code rather than accepting it blindly.&lt;/p&gt;

&lt;p&gt;I hope this article helps anyone considering Snowpark for data processing within Snowflake.&lt;/p&gt;

</description>
      <category>dataengineering</category>
      <category>snowflake</category>
    </item>
  </channel>
</rss>
