<?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>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; toyano
&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>
    <item>
      <title>Organizing How to Use AWS Glue Workflow</title>
      <dc:creator>Aki</dc:creator>
      <pubDate>Fri, 22 May 2026 13:38:32 +0000</pubDate>
      <link>https://dev.to/aws-builders/organizing-how-to-use-aws-glue-workflow-4n0j</link>
      <guid>https://dev.to/aws-builders/organizing-how-to-use-aws-glue-workflow-4n0j</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/5d8278b0d84881" rel="noopener noreferrer"&gt;AWS Glue Workflowの使い方について整理してみる&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;/p&gt;

&lt;p&gt;Previously, I wrote an article comparing when to use AWS Step Functions versus Glue Workflow.&lt;br&gt;
&lt;a href="https://dev.to/aws-builders/organizing-the-use-cases-of-aws-step-functions-and-glue-workflow-for-etl-processing-with-aws-glue-2o6b"&gt;Organizing the Use Cases of AWS Step Functions and Glue Workflow for ETL Processing with AWS Glue Jobs&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;As I mentioned there, I personally like Glue Workflow and consider it an excellent service that balances simplicity and low cost.&lt;/p&gt;

&lt;p&gt;However, in recent years, Step Functions has become increasingly mainstream, and I get the impression that opportunities to work with Glue Workflow have decreased.&lt;br&gt;
Because of that, I think many people are unsure about how they should actually use it in practice.&lt;/p&gt;

&lt;p&gt;So in this article, I’d like to organize everything from the basics of Glue Workflow to practical usage patterns.&lt;br&gt;
I hope this helps more people become interested in Glue Workflow.&lt;/p&gt;


&lt;h2&gt;
  
  
  What Is Glue Workflow?
&lt;/h2&gt;

&lt;p&gt;AWS Glue Workflow is Glue’s native workflow orchestration feature.&lt;br&gt;
It defines ETL pipelines by combining the following three elements:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Component&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 Job&lt;/td&gt;
&lt;td&gt;The actual ETL processing using Spark or Python Shell&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Glue Crawler&lt;/td&gt;
&lt;td&gt;Scans data sources such as S3 and registers table definitions in the Data Catalog&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Glue Trigger&lt;/td&gt;
&lt;td&gt;Defines execution conditions for Jobs and Crawlers (schedule, event, conditional, etc.)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;By connecting these components as a DAG (Directed Acyclic Graph), you can build ETL pipelines.&lt;/p&gt;

&lt;p&gt;Another characteristic is that workflows can be visually configured through the Glue console GUI.&lt;/p&gt;

&lt;p&gt;A major advantage is that workflows themselves incur no additional cost—you only pay for Job and Crawler execution.&lt;/p&gt;


&lt;h2&gt;
  
  
  Details of the Core Components
&lt;/h2&gt;

&lt;p&gt;The core of Glue Workflow is the Trigger mechanism.&lt;br&gt;
There are four trigger types, each with different roles.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Trigger Type&lt;/th&gt;
&lt;th&gt;Execution Condition&lt;/th&gt;
&lt;th&gt;Typical Use Case&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;SCHEDULED&lt;/td&gt;
&lt;td&gt;Scheduled execution using cron expressions (UTC, minimum 5-minute interval)&lt;/td&gt;
&lt;td&gt;Periodic execution such as daily ETL&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;ON_DEMAND&lt;/td&gt;
&lt;td&gt;Manual execution or via API/SDK&lt;/td&gt;
&lt;td&gt;Arbitrary execution timing&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;CONDITIONAL&lt;/td&gt;
&lt;td&gt;Triggered based on the status of preceding Jobs/Crawlers&lt;/td&gt;
&lt;td&gt;Chained execution between Jobs&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;EVENT&lt;/td&gt;
&lt;td&gt;Triggered by EventBridge events&lt;/td&gt;
&lt;td&gt;Event-driven pipelines&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The typical pattern is:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Use &lt;code&gt;SCHEDULED&lt;/code&gt;, &lt;code&gt;ON_DEMAND&lt;/code&gt;, or &lt;code&gt;EVENT&lt;/code&gt; as the workflow’s starting trigger&lt;/li&gt;
&lt;li&gt;Use &lt;code&gt;CONDITIONAL&lt;/code&gt; to connect downstream Jobs&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;By the way, AWS officially recommends keeping the number of elements included in a workflow (Jobs + Crawlers + Triggers) under 100.&lt;br&gt;
Exceeding this recommendation can cause errors when resuming or stopping Workflow Runs.&lt;/p&gt;


&lt;h2&gt;
  
  
  CONDITIONAL Trigger Usage Patterns
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;CONDITIONAL&lt;/code&gt; Triggers are one of the key features that provide flexibility in Glue Workflow.&lt;br&gt;
Here are several representative patterns.&lt;/p&gt;


&lt;h3&gt;
  
  
  1. Simple Sequential Pattern
&lt;/h3&gt;

&lt;p&gt;This is the most basic usage pattern: “Run JobB after JobA succeeds.”&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;JobA (SUCCEEDED) → JobB
&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%2Ft4lo1qb55pycmsypu3m7.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%2Ft4lo1qb55pycmsypu3m7.png" width="800" height="409"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;You can implement this simply by configuring a CONDITIONAL Trigger with the condition:&lt;br&gt;
“Start when JobA reaches the SUCCEEDED state.”&lt;/p&gt;


&lt;h3&gt;
  
  
  2. Waiting for Multiple Jobs to Complete (AND Condition)
&lt;/h3&gt;

&lt;p&gt;This pattern runs JobC only after both JobA and JobB succeed.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;JobA (SUCCEEDED) ┐
                 ├→ JobC
JobB (SUCCEEDED) ┘
&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%2Fb010g1ri50yqziptg5oh.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%2Fb010g1ri50yqziptg5oh.png" width="800" height="428"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;This can be implemented by setting &lt;code&gt;Logical: AND&lt;/code&gt; in the CONDITIONAL Trigger predicate and listing multiple conditions.&lt;/p&gt;

&lt;p&gt;This is useful in scenarios such as:&lt;br&gt;
“Run an aggregation Job only after multiple data sources have finished loading.”&lt;/p&gt;


&lt;h3&gt;
  
  
  3. Triggering When Any Job Completes (ANY Condition)
&lt;/h3&gt;

&lt;p&gt;This pattern runs JobC when either JobA or JobB succeeds.&lt;/p&gt;

&lt;p&gt;The predicate of a CONDITIONAL Trigger has a &lt;code&gt;Logical&lt;/code&gt; field where you can specify either &lt;code&gt;AND&lt;/code&gt; or &lt;code&gt;ANY&lt;/code&gt;.&lt;br&gt;
Using &lt;code&gt;ANY&lt;/code&gt; causes the trigger to fire as soon as any one of the specified conditions is satisfied.&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%2F6gz82gf7gzogtq3unvcc.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%2F6gz82gf7gzogtq3unvcc.png" width="800" height="418"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Note that although this behavior is logically equivalent to “OR,” the actual Glue configuration value is &lt;code&gt;ANY&lt;/code&gt;.&lt;br&gt;
This is important when defining workflows using IaC or CLI because specifying &lt;code&gt;OR&lt;/code&gt; will result in an error.&lt;/p&gt;


&lt;h3&gt;
  
  
  4. Failure Branching (Catching FAILED States)
&lt;/h3&gt;

&lt;p&gt;CONDITIONAL Triggers can react not only to &lt;code&gt;SUCCEEDED&lt;/code&gt;, but also to states such as:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;code&gt;FAILED&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;STOPPED&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;TIMEOUT&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;ERROR&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;(The supported states differ slightly between Jobs and Crawlers.)&lt;/p&gt;

&lt;p&gt;Using this feature, you can create patterns such as launching a notification Job (for example, a Python Shell Job that publishes to SNS) when a Job fails.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;JobA (SUCCEEDED) → Downstream Processing
JobA (FAILED)    → Notification Job
&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%2Fz2a65wew7c1kogj4sssa.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%2Fz2a65wew7c1kogj4sssa.png" width="800" height="461"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;For relatively simple error handling, this approach allows you to avoid introducing Step Functions.&lt;/p&gt;




&lt;h2&gt;
  
  
  Managing Parameters with &lt;code&gt;default_run_properties&lt;/code&gt;
&lt;/h2&gt;

&lt;p&gt;Glue Workflow provides a property called &lt;code&gt;default_run_properties&lt;/code&gt;, which acts like globally shared variables across the workflow.&lt;/p&gt;

&lt;h3&gt;
  
  
  How It Works
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;default_run_properties&lt;/code&gt; stores key-value pairs that can be referenced by all Jobs within the workflow.&lt;/p&gt;

&lt;p&gt;It functions as the default set of parameters passed during Workflow execution and serves as the foundation for sharing information between Jobs.&lt;/p&gt;

&lt;p&gt;One important note:&lt;br&gt;
Run Property values may appear in logs, so you should avoid storing secrets directly in them.&lt;/p&gt;

&lt;p&gt;Instead, retrieve secrets through services such as:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;AWS Secrets Manager&lt;/li&gt;
&lt;li&gt;Glue Connections&lt;/li&gt;
&lt;/ul&gt;


&lt;h3&gt;
  
  
  How to Configure It
&lt;/h3&gt;

&lt;p&gt;There are three main configuration methods.&lt;/p&gt;
&lt;h4&gt;
  
  
  Configure from the Console
&lt;/h4&gt;

&lt;p&gt;In the Glue console:&lt;br&gt;
Workflow → Edit Properties&lt;/p&gt;

&lt;p&gt;You can add key-value pairs there.&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%2F97w8stf6glxmlbk53h4v.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%2F97w8stf6glxmlbk53h4v.png" width="799" height="547"&gt;&lt;/a&gt;&lt;/p&gt;


&lt;h4&gt;
  
  
  Configure via boto3
&lt;/h4&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;boto3&lt;/span&gt;

&lt;span class="n"&gt;glue&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;boto3&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;client&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&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="n"&gt;glue&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;create_workflow&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;Name&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;my-workflow&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;DefaultRunProperties&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;env&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;production&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;target_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;2026-05-21&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;h4&gt;
  
  
  Configure via IaC (CloudFormation, etc.)
&lt;/h4&gt;

&lt;p&gt;You can specify &lt;code&gt;DefaultRunProperties&lt;/code&gt; in the &lt;code&gt;AWS::Glue::Workflow&lt;/code&gt; resource.&lt;/p&gt;


&lt;h3&gt;
  
  
  Static Parameters vs Dynamic Parameters
&lt;/h3&gt;

&lt;p&gt;Typical usage patterns include:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;Static parameters&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Environment names (&lt;code&gt;dev&lt;/code&gt; / &lt;code&gt;prod&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;S3 bucket names&lt;/li&gt;
&lt;li&gt;Values that rarely change&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Dynamic parameters&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Processing dates&lt;/li&gt;
&lt;li&gt;Execution IDs&lt;/li&gt;
&lt;li&gt;Values that change per execution&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Dynamic parameters can be updated either by:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Passing &lt;code&gt;RunProperties&lt;/code&gt; to &lt;code&gt;start_workflow_run&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Dynamically updating them later using &lt;code&gt;put_workflow_run_properties&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;


&lt;h2&gt;
  
  
  Passing Data Between Jobs
&lt;/h2&gt;

&lt;p&gt;Using &lt;code&gt;default_run_properties&lt;/code&gt; as the foundation, let’s look at how to exchange data between Jobs.&lt;/p&gt;


&lt;h3&gt;
  
  
  Dynamically Updating Run Properties
&lt;/h3&gt;

&lt;p&gt;During Job execution, you can dynamically update Workflow Run properties by calling the &lt;code&gt;put_workflow_run_properties&lt;/code&gt; API.&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;boto3&lt;/span&gt;

&lt;span class="n"&gt;glue&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;boto3&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;client&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&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="n"&gt;glue&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;put_workflow_run_properties&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;Name&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;my-workflow&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;RunId&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;workflow_run_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;RunProperties&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;processed_records&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;12345&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;output_path&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;s3://mybucket/output/2026-05-21/&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;This allows downstream Jobs to reference values calculated by upstream Jobs.&lt;/p&gt;




&lt;h3&gt;
  
  
  Retrieving Properties from a PySpark Job
&lt;/h3&gt;

&lt;p&gt;Inside a PySpark Job, you first retrieve the Workflow Run ID and then call &lt;code&gt;get_workflow_run_properties&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;import&lt;/span&gt; &lt;span class="n"&gt;sys&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;boto3&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="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="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;WORKFLOW_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;WORKFLOW_RUN_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="n"&gt;glue&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;boto3&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;client&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&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="n"&gt;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;glue&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get_workflow_run_properties&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;Name&lt;/span&gt;&lt;span class="o"&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;WORKFLOW_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;RunId&lt;/span&gt;&lt;span class="o"&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;WORKFLOW_RUN_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="n"&gt;properties&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;RunProperties&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;

&lt;span class="n"&gt;target_date&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;properties&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="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;target_date&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;code&gt;WORKFLOW_NAME&lt;/code&gt; and &lt;code&gt;WORKFLOW_RUN_ID&lt;/code&gt; are special arguments automatically passed when a Job is launched through a Workflow.&lt;/p&gt;




&lt;h3&gt;
  
  
  Retrieving Properties from a Python Shell Job
&lt;/h3&gt;

&lt;p&gt;The basic approach is the same for Python Shell Jobs:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Retrieve arguments using &lt;code&gt;getResolvedOptions&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Access properties through boto3&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Since Python Shell Jobs do not require SparkContext initialization, they can be written more lightweight.&lt;/p&gt;

&lt;p&gt;They are also cheaper than Spark-based Glue Jobs, so depending on the requirements, they can be a good option.&lt;/p&gt;

&lt;p&gt;Personally, I also like Python Shell Jobs, although I feel opportunities to use them in real-world projects have decreased, which is a bit unfortunate.&lt;/p&gt;

&lt;p&gt;I’ve also written articles about Python Shell Jobs, so feel free to check them out.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://dev.to/aws-builders/s3-triggers-how-to-launch-glue-python-shell-via-aws-lambda-4ke8"&gt;S3 Triggers: How to Launch Glue Python Shell via AWS Lambda&lt;/a&gt;&lt;/p&gt;




&lt;h3&gt;
  
  
  Anti-Patterns for Data Passing
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;default_run_properties&lt;/code&gt; should only be used for metadata-like information exchange.&lt;/p&gt;

&lt;p&gt;The following usage patterns should generally be avoided:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;Passing large datasets directly&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Store the data itself in S3 and pass only the path&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;li&gt;

&lt;p&gt;Storing secrets&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Use Secrets Manager or Glue Connections instead&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;li&gt;

&lt;p&gt;Frequently rewriting properties&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;This increases API calls and introduces race condition risks&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;/ul&gt;




&lt;h2&gt;
  
  
  Integrating with EventBridge and Other Services
&lt;/h2&gt;

&lt;p&gt;Glue Workflow becomes much more flexible when combined with EventBridge.&lt;/p&gt;




&lt;h3&gt;
  
  
  EventBridge-Based Startup (EVENT Trigger)
&lt;/h3&gt;

&lt;p&gt;Glue Workflow can be started directly by EventBridge events.&lt;/p&gt;

&lt;p&gt;This is achieved by setting the Trigger Type to &lt;code&gt;EVENT&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 glue create-trigger &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--workflow-name&lt;/span&gt; my-workflow &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--type&lt;/span&gt; EVENT &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--name&lt;/span&gt; s3-arrival-trigger &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--actions&lt;/span&gt; &lt;span class="nv"&gt;JobName&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;my-job
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;By configuring an EventBridge rule with Glue Workflow as the target, the workflow starts automatically when an event occurs.&lt;/p&gt;

&lt;p&gt;However, appropriate IAM permissions such as &lt;code&gt;glue:notifyEvent&lt;/code&gt; are required.&lt;/p&gt;




&lt;h3&gt;
  
  
  Batch Event Startup
&lt;/h3&gt;

&lt;p&gt;EVENT Triggers also support event batching.&lt;/p&gt;

&lt;p&gt;Using &lt;code&gt;EventBatchingCondition&lt;/code&gt;, you can configure the workflow to start when either:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;N events arrive&lt;/li&gt;
&lt;li&gt;M seconds pass since the first event arrived
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;aws glue create-trigger &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--workflow-name&lt;/span&gt; my-workflow &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--type&lt;/span&gt; EVENT &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--name&lt;/span&gt; batch-trigger &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--event-batching-condition&lt;/span&gt; &lt;span class="nv"&gt;BatchSize&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;10,BatchWindow&lt;span class="o"&gt;=&lt;/span&gt;300 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--actions&lt;/span&gt; &lt;span class="nv"&gt;JobName&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;my-job
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This enables patterns such as:&lt;br&gt;
“Run ETL once 100 files have arrived.”&lt;/p&gt;

&lt;p&gt;The maximum batch window is 900 seconds (15 minutes).&lt;/p&gt;


&lt;h3&gt;
  
  
  Starting from S3 Events (The Parameter Passing Limitation)
&lt;/h3&gt;

&lt;p&gt;A common use case is:&lt;br&gt;
“Start a workflow when a file is uploaded to S3.”&lt;/p&gt;

&lt;p&gt;When starting Glue Workflow through EventBridge, the event IDs are automatically stored in a Run Property called &lt;code&gt;aws:eventIds&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="n"&gt;event_ids&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;glue_client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get_workflow_run_properties&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;Name&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;workflow_name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;RunId&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;workflow_run_id&lt;/span&gt;
&lt;span class="p"&gt;)[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;RunProperties&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;aws:eventIds&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 returned value looks like:&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="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;abc-123&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="s"&gt;def-456&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;However, this is where one of Glue Workflow’s limitations becomes apparent.&lt;/p&gt;

&lt;p&gt;The EventBridge event payload itself (such as the S3 object key or bucket name) is not automatically passed as Run Properties.&lt;/p&gt;

&lt;p&gt;Only the event IDs are provided.&lt;/p&gt;

&lt;p&gt;If you need the actual object details, your Job must retrieve the corresponding event contents from CloudTrail, which becomes somewhat cumbersome.&lt;/p&gt;

&lt;p&gt;Because of this, many cases are easier to manage by placing Lambda in the middle and explicitly calling &lt;code&gt;start_workflow_run&lt;/code&gt; with structured Run Properties.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;S3 PUT → EventBridge → Lambda → start_workflow_run (pass parameters via RunProperties)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Example Lambda code:&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;boto3&lt;/span&gt;

&lt;span class="n"&gt;glue&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;boto3&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;client&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&lt;/span&gt;&lt;span class="sh"&gt;'&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;lambda_handler&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;event&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;bucket&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;event&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;detail&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;bucket&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="n"&gt;key&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;event&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;detail&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;object&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;key&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;

    &lt;span class="n"&gt;glue&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;start_workflow_run&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;Name&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;my-workflow&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;RunProperties&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;source_bucket&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;bucket&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;source_key&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;key&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;With Step Functions, the EventBridge payload can be received directly using paths such as &lt;code&gt;$.detail&lt;/code&gt;, so there is no need for an intermediate Lambda function.&lt;/p&gt;

&lt;p&gt;This is one of the areas where Glue Workflow limitations become noticeable compared to Step Functions.&lt;/p&gt;




&lt;h3&gt;
  
  
  Detecting Workflow Completion via EventBridge
&lt;/h3&gt;

&lt;p&gt;Glue Workflow status changes are emitted as EventBridge events.&lt;/p&gt;

&lt;p&gt;You can use this to:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Send SNS notifications&lt;/li&gt;
&lt;li&gt;Trigger downstream systems&lt;/li&gt;
&lt;li&gt;Launch post-processing workflows
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Glue Workflow (COMPLETED/FAILED)
    → EventBridge
        → SNS / Lambda
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is especially useful when you want separate processing for success and failure cases.&lt;/p&gt;




&lt;h3&gt;
  
  
  Calling Glue Workflow from Step Functions
&lt;/h3&gt;

&lt;p&gt;It is also possible to launch Glue Workflow from Step Functions.&lt;/p&gt;

&lt;p&gt;However, since there is no &lt;code&gt;.sync&lt;/code&gt; integration pattern available, you must implement your own polling logic to detect completion.&lt;/p&gt;

&lt;p&gt;I covered this in detail in the previous article, so feel free to refer to it if interested.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://dev.to/aws-builders/organizing-the-use-cases-of-aws-step-functions-and-glue-workflow-for-etl-processing-with-aws-glue-2o6b"&gt;Organizing the Use Cases of AWS Step Functions and Glue Workflow for ETL Processing with AWS Glue Jobs&lt;/a&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  Operational Tips
&lt;/h2&gt;

&lt;p&gt;Here are several practical points worth knowing when operating Glue Workflow in production.&lt;/p&gt;




&lt;h3&gt;
  
  
  Resuming Failed Workflows (&lt;code&gt;ResumeWorkflowRun&lt;/code&gt;)
&lt;/h3&gt;

&lt;p&gt;Glue Workflow provides a feature called &lt;code&gt;ResumeWorkflowRun&lt;/code&gt;, which allows resuming execution from failed nodes.&lt;/p&gt;

&lt;p&gt;In the console, you can:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Open the failed Workflow Run detail page&lt;/li&gt;
&lt;li&gt;Select the nodes to resume&lt;/li&gt;
&lt;li&gt;Enable the “Resume” checkbox&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;It is also available through CLI/API.&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 resume-workflow-run &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--name&lt;/span&gt; my-workflow &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--run-id&lt;/span&gt; wr_xxxx &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--node-ids&lt;/span&gt; node_yyyy node_zzzz
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;When resumed:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The specified nodes&lt;/li&gt;
&lt;li&gt;And all downstream nodes&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;are re-executed.&lt;/p&gt;

&lt;p&gt;The resumed workflow is tracked using a new Run ID.&lt;/p&gt;

&lt;p&gt;Compared with Step Functions Redrive, however, there are several limitations:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;You must explicitly specify failed nodes&lt;/li&gt;
&lt;li&gt;Retrieving node IDs requires calling:
&lt;code&gt;get-workflow-run --include-graph&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Additional IAM permissions (&lt;code&gt;glue:ResumeWorkflowRun&lt;/code&gt;) are required&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For simple retry scenarios, restarting the entire workflow via &lt;code&gt;ON_DEMAND&lt;/code&gt; execution is often easier.&lt;/p&gt;

&lt;p&gt;For pipelines with more sophisticated recovery requirements, Step Functions tends to provide a smoother operational experience.&lt;/p&gt;




&lt;h3&gt;
  
  
  Monitoring with CloudWatch
&lt;/h3&gt;

&lt;p&gt;Glue Workflow execution states can be viewed in the Glue console, but detailed Job and Crawler logs are output to CloudWatch Logs.&lt;/p&gt;

&lt;p&gt;Typical monitoring targets include:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;Workflow Run list&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Glue Console&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;li&gt;

&lt;p&gt;Job execution logs&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;code&gt;/aws-glue/jobs/output&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;/aws-glue/jobs/error&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;li&gt;

&lt;p&gt;Metrics&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;CloudWatch Metrics&lt;/li&gt;
&lt;li&gt;Execution duration&lt;/li&gt;
&lt;li&gt;DPU usage&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;/ul&gt;

&lt;p&gt;In practice, monitoring and alerting strategies often combine:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Workflow-level statuses&lt;/li&gt;
&lt;li&gt;Job-level metrics&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;depending on the situation.&lt;/p&gt;




&lt;h3&gt;
  
  
  Handling the 100-Object Recommendation Limit
&lt;/h3&gt;

&lt;p&gt;As mentioned earlier, AWS recommends keeping the total number of objects in a workflow (Jobs + Crawlers + Triggers) below 100.&lt;/p&gt;

&lt;p&gt;If a large pipeline approaches this limit, consider:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;Splitting workflows&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Trigger downstream workflows after upstream completion&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;li&gt;

&lt;p&gt;Consolidating common processing inside Jobs&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Merge smaller processing units into larger Jobs&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;/ul&gt;

&lt;p&gt;In my experience, workflows approaching 100 objects are often already too complex from a design perspective.&lt;br&gt;
At that point, it may be worth reconsidering the architecture itself or migrating to Step Functions.&lt;/p&gt;




&lt;h2&gt;
  
  
  Where Glue Workflow Fits — and Its Limitations
&lt;/h2&gt;

&lt;p&gt;Glue Workflow shines in scenarios such as:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Simple ETL pipelines completed entirely within Glue Jobs and Crawlers&lt;/li&gt;
&lt;li&gt;Periodic ingestion pipelines for data lakes&lt;/li&gt;
&lt;li&gt;Lightweight small-to-medium scale pipelines that need to be launched quickly&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;On the other hand, Step Functions is generally better suited for:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Integrations involving Lambda, ECS, and other AWS services&lt;/li&gt;
&lt;li&gt;Complex branching and dynamic parameter control&lt;/li&gt;
&lt;li&gt;Large-scale development involving multiple teams&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I discussed these decision criteria in more detail in the previous article, so feel free to refer to it.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://dev.to/aws-builders/organizing-the-use-cases-of-aws-step-functions-and-glue-workflow-for-etl-processing-with-aws-glue-2o6b"&gt;Organizing the Use Cases of AWS Step Functions and Glue Workflow for ETL Processing with AWS Glue Jobs&lt;/a&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;In this article, I organized AWS Glue Workflow from the basics through practical usage patterns.&lt;/p&gt;

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

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Four trigger types:&lt;br&gt;
&lt;code&gt;SCHEDULED&lt;/code&gt; / &lt;code&gt;ON_DEMAND&lt;/code&gt; / &lt;code&gt;CONDITIONAL&lt;/code&gt; / &lt;code&gt;EVENT&lt;/code&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;CONDITIONAL Triggers:&lt;br&gt;
Flexible flow control using &lt;code&gt;AND&lt;/code&gt; / &lt;code&gt;ANY&lt;/code&gt; conditions and failure branching&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;code&gt;default_run_properties&lt;/code&gt;:&lt;br&gt;
Shared workflow-wide parameter management&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Data passing between Jobs:&lt;br&gt;
Dynamic value propagation using &lt;code&gt;put_workflow_run_properties&lt;/code&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;EventBridge integration:&lt;br&gt;
Event-driven execution via EVENT Trigger&lt;br&gt;
(although Lambda-based parameter passing is often easier)&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;code&gt;ResumeWorkflowRun&lt;/code&gt;:&lt;br&gt;
Partial restart functionality from failed nodes&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;After writing all of this, I still have to admit:&lt;br&gt;
Step Functions is generally easier to use.&lt;/p&gt;

&lt;p&gt;That said, Glue Workflow still provides meaningful value today because it allows you to build Glue-centric ETL pipelines in a very simple and cost-efficient way.&lt;/p&gt;

&lt;p&gt;Rather than defaulting to Step Functions automatically, understanding Glue Workflow properly and knowing when to use it can broaden your architectural options.&lt;/p&gt;

&lt;p&gt;I hope this article helps both people who are starting to use Glue Workflow and those already working with it.&lt;/p&gt;

</description>
      <category>aws</category>
      <category>dataengineering</category>
    </item>
    <item>
      <title>Organizing the Use Cases of AWS Step Functions and Glue Workflow for ETL Processing with AWS Glue Jobs</title>
      <dc:creator>Aki</dc:creator>
      <pubDate>Wed, 20 May 2026 12:09:21 +0000</pubDate>
      <link>https://dev.to/aws-builders/organizing-the-use-cases-of-aws-step-functions-and-glue-workflow-for-etl-processing-with-aws-glue-2o6b</link>
      <guid>https://dev.to/aws-builders/organizing-the-use-cases-of-aws-step-functions-and-glue-workflow-for-etl-processing-with-aws-glue-2o6b</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/47fd9b49b418a9" rel="noopener noreferrer"&gt;Glue JobのETL処理におけるAWS Step FunctionsとGlue Workflowの使い分けを整理する&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;/p&gt;

&lt;p&gt;When building data pipelines on AWS, deciding which workflow orchestration tool to use is an important architectural decision.&lt;br&gt;
Especially when designing ETL pipelines centered around Glue Jobs, the following two services are commonly compared:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;AWS Step Functions&lt;/li&gt;
&lt;li&gt;AWS Glue Workflow&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Both services are workflow orchestration tools that manage job dependencies and execute processes sequentially.&lt;br&gt;
However, since they are designed with different strengths and philosophies, choosing the right one based on your requirements is critical.&lt;/p&gt;

&lt;p&gt;In this article, I’ll organize the characteristics, advantages, disadvantages, and use cases of these two services, and discuss how to decide which one to choose.&lt;/p&gt;

&lt;p&gt;Personally, I really like Glue Workflow, but I’ve had fewer opportunities to use it recently, which is honestly a bit disappointing.&lt;br&gt;
I also like Glue Python Shell, but compared to standard Glue Jobs, I rarely get to use it these days either...&lt;/p&gt;

&lt;h2&gt;
  
  
  Major Workflow Orchestration Tools
&lt;/h2&gt;

&lt;p&gt;AWS provides multiple workflow orchestration services, but the two most commonly compared for Glue Job–based data pipelines are the following:&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;Overview&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;AWS Step Functions&lt;/td&gt;
&lt;td&gt;AWS managed workflow orchestration service. Can flexibly orchestrate a wide range of AWS services such as Lambda, Glue, ECS, and more&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;AWS Glue Workflow&lt;/td&gt;
&lt;td&gt;Glue-native workflow feature. Defines pipelines using Glue Jobs, Crawlers, and Triggers&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;AWS also offers Amazon Managed Workflows for Apache Airflow (MWAA).&lt;br&gt;
MWAA becomes a strong option when you need highly complex dependency management or cross-cloud orchestration. However, in this article, I’ll focus specifically on comparing Step Functions and Glue Workflow.&lt;/p&gt;

&lt;p&gt;Recently, many teams have also adopted workflow tools such as Airflow, Dagster, and Prefect. As always, selecting the right tool depends heavily on your requirements and goals.&lt;/p&gt;

&lt;h2&gt;
  
  
  AWS Step Functions
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Overview
&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%2Flijyw5w6x21p2ml3f47w.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%2Flijyw5w6x21p2ml3f47w.png" width="799" height="410"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;(Figure above: Visual workflow definition using Workflow Studio)&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;AWS Step Functions is a workflow orchestration service that defines workflows using Amazon States Language (ASL), a JSON/YAML-based language.&lt;/p&gt;

&lt;p&gt;It integrates with over 200 AWS services including Lambda, Glue, ECS, SNS, and DynamoDB, and supports flexible workflow controls such as conditional branching, parallel execution, and retry handling.&lt;/p&gt;

&lt;h3&gt;
  
  
  Features
&lt;/h3&gt;

&lt;p&gt;Step Functions provides two workflow types:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&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;Standard Workflow&lt;/td&gt;
&lt;td&gt;Long-running execution, audit logs, exactly-once execution guarantee&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Express Workflow&lt;/td&gt;
&lt;td&gt;High throughput, low cost, optimized for short-lived processing (at-least-once)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;In the context of data pipelines, Standard Workflow is generally the more common choice.&lt;/p&gt;

&lt;p&gt;Step Functions also provides AWS Step Functions Workflow Studio, a visual editor that allows workflows to be built through a GUI.&lt;/p&gt;

&lt;h3&gt;
  
  
  Advantages
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Multi-service integration: Easily integrates Glue with other AWS services such as Lambda, ECS, SNS, and more&lt;/li&gt;
&lt;li&gt;Flexible control flow: Supports conditional branching (Choice), parallel execution (Parallel/Map), and advanced error handling (Catch/Retry)&lt;/li&gt;
&lt;li&gt;High observability: Execution history and per-step states can be visually inspected in the console&lt;/li&gt;
&lt;li&gt;Event-driven integration: Easily triggered through EventBridge using S3 uploads or schedules&lt;/li&gt;
&lt;li&gt;Parallel execution support (Map): Well suited for large-scale processing such as file-level or partition-level parallel execution. Distributed Map is especially useful for high-scale parallel workloads&lt;/li&gt;
&lt;li&gt;Infrastructure as Code support: Can be fully managed through CloudFormation, CDK, or Terraform&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Disadvantages
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Learning curve: Requires understanding ASL, and complex workflows can become verbose&lt;/li&gt;
&lt;li&gt;Cost: Standard Workflow pricing is based on state transitions, so costs can increase with larger workflows&lt;/li&gt;
&lt;li&gt;Glue integration setup: IAM roles, parameter passing, and Glue Job integration must be configured manually&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Use Cases
&lt;/h3&gt;

&lt;p&gt;Step Functions is a strong fit for scenarios such as:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Hybrid pipelines combining Glue with Lambda, ECS, or other AWS services&lt;/li&gt;
&lt;li&gt;Complex workflows requiring branching, parallel processing, or dynamic parameter passing&lt;/li&gt;
&lt;li&gt;Pipelines that require SNS notifications or compensating actions on failures&lt;/li&gt;
&lt;li&gt;Teams managing infrastructure strictly through IaC&lt;/li&gt;
&lt;li&gt;Large-scale data platforms involving multiple teams&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  AWS Glue Workflow
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Overview
&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%2Fgb8t1vgeas2xynimxmo8.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%2Fgb8t1vgeas2xynimxmo8.png" width="800" height="419"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;(Figure above: Visual workflow definition using Glue Workflow)&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;AWS Glue Workflow is Glue’s native workflow orchestration feature.&lt;/p&gt;

&lt;p&gt;It defines pipelines using three primary components:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Glue Jobs&lt;/li&gt;
&lt;li&gt;Glue Crawlers&lt;/li&gt;
&lt;li&gt;Glue Triggers&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Pipelines can be configured directly from the Glue console GUI, making it easy to build Glue-centric ETL workflows quickly.&lt;/p&gt;

&lt;h3&gt;
  
  
  Features
&lt;/h3&gt;

&lt;p&gt;The major components of Glue Workflow are as follows:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Component&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 Job&lt;/td&gt;
&lt;td&gt;Actual ETL processing using Spark or Python Shell&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Glue Crawler&lt;/td&gt;
&lt;td&gt;Scans data sources such as S3 and registers table metadata into the Data Catalog&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Glue Trigger&lt;/td&gt;
&lt;td&gt;Defines execution conditions such as schedules, events, or conditional dependencies&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Glue Triggers support three types:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;code&gt;SCHEDULED&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;ON_DEMAND&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;CONDITIONAL&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;EVENT&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This allows chained execution patterns, such as triggering downstream jobs based on upstream job success or failure.&lt;/p&gt;

&lt;h3&gt;
  
  
  Advantages
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Native Glue integration: Seamlessly integrates with Glue Jobs and Crawlers with minimal additional setup&lt;/li&gt;
&lt;li&gt;Simple configuration: DAG-style workflows can be built intuitively through the console GUI&lt;/li&gt;
&lt;li&gt;Low cost: No additional charge for the workflow itself beyond Glue Job execution costs&lt;/li&gt;
&lt;li&gt;Easy crawler integration: Natural fit for workflows that update the Data Catalog after ETL execution&lt;/li&gt;
&lt;li&gt;Glue Data Catalog integration: Job execution metadata and lineage can be managed centrally within Glue&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Disadvantages
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Glue-only orchestration: Cannot directly orchestrate Lambda, ECS, or other non-Glue services&lt;/li&gt;
&lt;li&gt;Limited event-driven capabilities: Primarily Trigger- and schedule-based, making advanced event integration less flexible than Step Functions&lt;/li&gt;
&lt;li&gt;Limited control flow: Weak support for advanced branching and dynamic parameter handling&lt;/li&gt;
&lt;li&gt;Observability limitations: Detailed execution logs often require separate CloudWatch investigation&lt;/li&gt;
&lt;li&gt;More difficult IaC management: CloudFormation and Terraform management can become cumbersome compared to Step Functions&lt;/li&gt;
&lt;li&gt;Limited parallel execution control: Not ideal for fine-grained parallelization or Map-style orchestration&lt;/li&gt;
&lt;li&gt;Weaker retry/re-execution control: Re-running only failed portions of a workflow is less flexible than in Step Functions&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Use Cases
&lt;/h3&gt;

&lt;p&gt;Glue Workflow is well suited for:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Simple ETL pipelines composed entirely of Glue Jobs and Crawlers&lt;/li&gt;
&lt;li&gt;Periodic ingestion pipelines into S3-based data lakes with automatic catalog updates&lt;/li&gt;
&lt;li&gt;Small teams or early-stage projects that need fast implementation&lt;/li&gt;
&lt;li&gt;Organizations that prefer operating primarily through the Glue console&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Which One Should You Choose?
&lt;/h2&gt;

&lt;p&gt;Based on the characteristics of both services, the selection criteria can generally be summarized as follows.&lt;/p&gt;

&lt;h3&gt;
  
  
  When to Choose Glue Workflow
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Your ETL pipeline consists only of Glue Jobs and Crawlers&lt;/li&gt;
&lt;li&gt;Simple sequential execution and conditional triggers are sufficient&lt;/li&gt;
&lt;li&gt;You want to build quickly (prototypes or small-scale projects)&lt;/li&gt;
&lt;li&gt;Your operations are centered around the Glue console&lt;/li&gt;
&lt;li&gt;You want to minimize costs&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  When to Choose Step Functions
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;You need integration with Lambda, ECS, or other AWS services&lt;/li&gt;
&lt;li&gt;You require branching, parallel processing, or advanced error handling&lt;/li&gt;
&lt;li&gt;You are adopting an EventBridge-centric event-driven architecture&lt;/li&gt;
&lt;li&gt;You want strict Infrastructure as Code management&lt;/li&gt;
&lt;li&gt;Multiple teams are involved in operating the data platform&lt;/li&gt;
&lt;li&gt;Observability and audit logging are important&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Summary of Decision Criteria
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Perspective&lt;/th&gt;
&lt;th&gt;Glue Workflow&lt;/th&gt;
&lt;th&gt;Step Functions&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Supported Services&lt;/td&gt;
&lt;td&gt;Glue only&lt;/td&gt;
&lt;td&gt;Broad AWS integration&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Control Flow&lt;/td&gt;
&lt;td&gt;Simple&lt;/td&gt;
&lt;td&gt;Flexible and advanced&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Observability&lt;/td&gt;
&lt;td&gt;△&lt;/td&gt;
&lt;td&gt;◎&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Ease of Configuration&lt;/td&gt;
&lt;td&gt;◎&lt;/td&gt;
&lt;td&gt;△ (learning curve)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Cost&lt;/td&gt;
&lt;td&gt;Low&lt;/td&gt;
&lt;td&gt;Depends on state transitions&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;IaC Management&lt;/td&gt;
&lt;td&gt;△&lt;/td&gt;
&lt;td&gt;◎&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Crawler Integration&lt;/td&gt;
&lt;td&gt;◎&lt;/td&gt;
&lt;td&gt;△ (manual setup required)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Glue Workflow is fundamentally Trigger-oriented and is not designed for advanced event orchestration like Step Functions.&lt;/p&gt;

&lt;p&gt;Unless Glue Workflow specifically satisfies your requirements better, choosing Step Functions is generally the safer long-term option.&lt;br&gt;
Personally, when designing architectures, I often start by considering Step Functions first.&lt;/p&gt;

&lt;p&gt;That said, Glue Workflow remains a strong choice when the requirement is simply:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;“I want to quickly build a Glue-centric ETL pipeline.”&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Combining Both Services
&lt;/h2&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%2F7lfz55jfm7tei3v78ssk.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%2F7lfz55jfm7tei3v78ssk.png" width="800" height="415"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Instead of choosing one or the other exclusively, it is also possible to invoke Glue Workflow from Step Functions.&lt;/p&gt;

&lt;p&gt;For example, Step Functions can handle preprocessing and postprocessing with Lambda, while delegating the ETL core to Glue Workflow.&lt;/p&gt;

&lt;p&gt;However, this introduces additional complexity because workflow state coordination between the two services must be managed carefully.&lt;br&gt;
If simplicity is important, standardizing on one orchestration tool is generally easier operationally.&lt;/p&gt;

&lt;h3&gt;
  
  
  Caveats When Invoking Glue Workflow from Step Functions
&lt;/h3&gt;

&lt;h4&gt;
  
  
  1. Completion Detection Requires Polling
&lt;/h4&gt;

&lt;p&gt;Step Functions provides a convenient &lt;code&gt;.sync&lt;/code&gt; integration pattern for &lt;code&gt;Glue: StartJobRun&lt;/code&gt;, which waits for job completion automatically.&lt;/p&gt;

&lt;p&gt;However, &lt;code&gt;Glue: StartWorkflowRun&lt;/code&gt; does &lt;strong&gt;not&lt;/strong&gt; support the &lt;code&gt;.sync&lt;/code&gt; integration pattern.&lt;/p&gt;

&lt;p&gt;While you can invoke Glue Workflow through SDK integration, Step Functions will immediately proceed to the next state without waiting for completion.&lt;br&gt;
As a result, you must implement custom polling logic to repeatedly check the WorkflowRun status.&lt;/p&gt;

&lt;h4&gt;
  
  
  2. Polling Logic Becomes Complex
&lt;/h4&gt;

&lt;p&gt;You typically need to implement a loop like:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Wait&lt;/li&gt;
&lt;li&gt;GetWorkflowRun&lt;/li&gt;
&lt;li&gt;Choice (RUNNING / COMPLETED / FAILED)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This increases both the number of states and the verbosity of the ASL definition.&lt;/p&gt;

&lt;h4&gt;
  
  
  3. Error Handling Becomes More Complicated
&lt;/h4&gt;

&lt;p&gt;Glue Workflow status is returned at the WorkflowRun level rather than the individual Job level.&lt;/p&gt;

&lt;p&gt;As a result, identifying which specific Glue Job failed requires additional parsing logic against the &lt;code&gt;GetWorkflowRun&lt;/code&gt; response.&lt;/p&gt;

&lt;p&gt;Because of these complications, although combining both services is technically possible, I generally recommend avoiding Step Functions → Glue Workflow orchestration unless there is a compelling reason.&lt;/p&gt;

&lt;p&gt;One possible use case is when you need to extend an existing Glue Workflow–based system incrementally.&lt;br&gt;
Even then, rebuilding the orchestration directly in Step Functions using existing Glue Jobs often feels cleaner.&lt;/p&gt;

&lt;h2&gt;
  
  
  Migration Considerations
&lt;/h2&gt;

&lt;p&gt;Some teams initially adopt Glue Workflow during the early stages of a project and later migrate to Step Functions as the data platform grows.&lt;/p&gt;

&lt;p&gt;When migrating, the following considerations become important:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Workflow definitions must be rewritten: Glue Trigger definitions need to be converted into ASL&lt;/li&gt;
&lt;li&gt;IAM roles must be redesigned: Step Functions requires permissions to invoke Glue Jobs&lt;/li&gt;
&lt;li&gt;Extensive testing is necessary: Existing jobs must be validated carefully after migration&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Migration is certainly possible, but it is not trivial.&lt;br&gt;
This is why it is important to consider future extensibility and maintainability from the beginning when selecting your orchestration tool.&lt;/p&gt;

&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;In this article, I organized the differences between AWS Step Functions and Glue Workflow for ETL orchestration.&lt;/p&gt;

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

&lt;ul&gt;
&lt;li&gt;Glue Workflow: Glue-native, simple, low-cost, and ideal for rapidly building straightforward ETL pipelines&lt;/li&gt;
&lt;li&gt;Step Functions: Better suited for multi-service orchestration, advanced workflow control, observability, and large-scale pipelines&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Neither service is universally “better.”&lt;br&gt;
The right choice depends on your use cases, organizational structure, operational requirements, and future scalability needs.&lt;/p&gt;

&lt;p&gt;For small-scale ETL pipelines, Glue Workflow is often sufficient. However, as data platforms evolve, requirements such as exception handling, notifications, conditional branching, and integrations with other services tend to grow over time. In many cases, architectures gradually move toward Step Functions as complexity increases.&lt;/p&gt;

&lt;p&gt;A practical strategy can be to start simple with Glue Workflow during the early stages, and later migrate to Step Functions when requirements become more sophisticated.&lt;/p&gt;

&lt;p&gt;That said, considering future migration costs, building on Step Functions from the beginning can also be a very reasonable approach.&lt;/p&gt;

&lt;p&gt;I hope this article helps anyone currently evaluating workflow orchestration options on AWS.&lt;/p&gt;

</description>
      <category>aws</category>
      <category>dataengineering</category>
    </item>
    <item>
      <title>Differences Between Snowflake Editions and Secure Connectivity with AWS</title>
      <dc:creator>Aki</dc:creator>
      <pubDate>Fri, 15 May 2026 12:55:34 +0000</pubDate>
      <link>https://dev.to/aws-builders/differences-between-snowflake-editions-and-secure-connectivity-with-aws-37ob</link>
      <guid>https://dev.to/aws-builders/differences-between-snowflake-editions-and-secure-connectivity-with-aws-37ob</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/87d4454b2d04fe" rel="noopener noreferrer"&gt;Snowflakeのエディションごとの違いとAWSとのセキュアな接続方法&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;/p&gt;

&lt;p&gt;More and more organizations are adopting Snowflake as their data platform.&lt;br&gt;
However, once you actually start planning an implementation, there is often a surprisingly common question:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;“Which Snowflake edition should we choose?”&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;In particular, many teams struggle to decide between Enterprise Edition and Business Critical Edition.&lt;/p&gt;

&lt;p&gt;This becomes even more important when Snowflake is used together with AWS and there is a requirement for secure network connectivity.&lt;/p&gt;

&lt;p&gt;Snowflake offers four editions, and higher editions provide stronger capabilities around security, compliance, and availability.&lt;br&gt;
In addition, private connectivity using AWS PrivateLink requires Business Critical Edition or higher.&lt;/p&gt;

&lt;p&gt;This means that if you initially start with Enterprise Edition and later realize that you need PrivateLink, migrating afterward can introduce additional operational and architectural effort.&lt;/p&gt;

&lt;p&gt;In this article, I would like to briefly organize the differences between Snowflake editions and then explore how to securely connect Snowflake with AWS using Business Critical Edition, especially through AWS PrivateLink.&lt;/p&gt;


&lt;h1&gt;
  
  
  Differences Between Snowflake Editions (Quick Overview)
&lt;/h1&gt;

&lt;p&gt;Snowflake provides the following four editions.&lt;br&gt;
Each higher edition includes all functionality from the lower editions, while credit pricing increases accordingly.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Edition&lt;/th&gt;
&lt;th&gt;Positioning&lt;/th&gt;
&lt;th&gt;Major Additional Features&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Standard&lt;/td&gt;
&lt;td&gt;Entry-level&lt;/td&gt;
&lt;td&gt;Core features, Time Travel (1 day), SSO, Network Policies&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Enterprise&lt;/td&gt;
&lt;td&gt;Large-scale / Production workloads&lt;/td&gt;
&lt;td&gt;Multi-cluster warehouses, Materialized Views, Time Travel (up to 90 days), Column- and Row-level security&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Business Critical&lt;/td&gt;
&lt;td&gt;Regulated industries&lt;/td&gt;
&lt;td&gt;Tri-Secret Secure, AWS PrivateLink, Failover, HIPAA / PCI DSS&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Virtual Private Snowflake (VPS)&lt;/td&gt;
&lt;td&gt;Highest isolation level&lt;/td&gt;
&lt;td&gt;Fully isolated Snowflake environment with dedicated infrastructure&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;


&lt;h2&gt;
  
  
  Standard Edition
&lt;/h2&gt;

&lt;p&gt;This is the entry-level edition and provides the core Snowflake functionality.&lt;/p&gt;

&lt;p&gt;It includes capabilities such as:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;SQL processing&lt;/li&gt;
&lt;li&gt;Semi-structured data support&lt;/li&gt;
&lt;li&gt;Data sharing&lt;/li&gt;
&lt;li&gt;1-day Time Travel&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For organizations simply looking to use Snowflake as a data warehouse, Standard Edition is often sufficient.&lt;/p&gt;

&lt;p&gt;It is generally a good fit for startups or smaller analytics teams that want to begin using Snowflake quickly.&lt;/p&gt;


&lt;h2&gt;
  
  
  Enterprise Edition
&lt;/h2&gt;

&lt;p&gt;Enterprise Edition adds several features that are effectively essential for production workloads.&lt;/p&gt;

&lt;p&gt;Key additions include:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Multi-cluster warehouses (for scaling concurrent users)&lt;/li&gt;
&lt;li&gt;Materialized Views&lt;/li&gt;
&lt;li&gt;Extended Time Travel (up to 90 days)&lt;/li&gt;
&lt;li&gt;Column-level and row-level security&lt;/li&gt;
&lt;li&gt;Dynamic Data Masking&lt;/li&gt;
&lt;li&gt;Query Acceleration Service&lt;/li&gt;
&lt;li&gt;Search Optimization Service&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;In practice, Enterprise Edition often feels like the real starting point for production Snowflake environments.&lt;/p&gt;

&lt;p&gt;As data volume and concurrency increase, multi-cluster warehouses become especially important.&lt;/p&gt;


&lt;h2&gt;
  
  
  Business Critical Edition
&lt;/h2&gt;

&lt;p&gt;Business Critical Edition is designed for organizations handling regulated or highly sensitive data.&lt;/p&gt;

&lt;p&gt;In addition to all Enterprise features, it includes:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;AWS PrivateLink / Azure Private Link / Google Private Service Connect&lt;/li&gt;
&lt;li&gt;Tri-Secret Secure (dual encryption using customer-managed keys)&lt;/li&gt;
&lt;li&gt;Account failover / failback&lt;/li&gt;
&lt;li&gt;Client redirect&lt;/li&gt;
&lt;li&gt;Compliance support for HIPAA, HITRUST CSF, PCI DSS, FedRAMP&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you are handling sensitive information such as:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;PHI (medical data)&lt;/li&gt;
&lt;li&gt;PCI-related cardholder data&lt;/li&gt;
&lt;li&gt;Personally identifiable information&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;then Business Critical Edition becomes necessary.&lt;/p&gt;

&lt;p&gt;It is also mandatory if your networking requirements specify that connectivity must avoid the public internet and use PrivateLink instead.&lt;/p&gt;

&lt;p&gt;Data is one of the most important enterprise assets.&lt;br&gt;
For that reason alone, I often see organizations adopting Business Critical Edition.&lt;/p&gt;


&lt;h2&gt;
  
  
  Virtual Private Snowflake (VPS)
&lt;/h2&gt;

&lt;p&gt;This is Snowflake’s highest edition and provides a dedicated Snowflake environment.&lt;/p&gt;

&lt;p&gt;Infrastructure is completely isolated and hardware resources are not shared with other customers.&lt;/p&gt;

&lt;p&gt;It is intended for organizations with extremely strict security requirements, such as:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Financial institutions&lt;/li&gt;
&lt;li&gt;Government agencies&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I personally have not worked with VPS directly, and pricing/details require contacting Snowflake, so I will omit deeper discussion here.&lt;/p&gt;


&lt;h2&gt;
  
  
  Which Edition Should You Choose?
&lt;/h2&gt;

&lt;p&gt;From a practical perspective, the decision criteria often look something like this:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Standard&lt;/strong&gt;: Evaluation, PoC, small analytics teams&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Enterprise&lt;/strong&gt;: General production workloads (this is where many companies start)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Business Critical&lt;/strong&gt;: Regulated industries, sensitive data, or mandatory PrivateLink requirements&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;VPS&lt;/strong&gt;: Financial/government environments requiring complete isolation&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;One important point is that if you simply decide to “start with Enterprise,” you cannot use PrivateLink.&lt;/p&gt;

&lt;p&gt;Because of this, it is safest to validate networking requirements at the beginning of the project.&lt;/p&gt;

&lt;p&gt;Upgrading later is possible, but it may require revisiting architecture and pricing assumptions.&lt;/p&gt;


&lt;h1&gt;
  
  
  Considering Connectivity with AWS
&lt;/h1&gt;

&lt;p&gt;When using Snowflake on AWS, network design—specifically how clients and applications connect—is tightly related to edition selection.&lt;/p&gt;

&lt;p&gt;Here, I would like to organize connectivity approaches from both the Enterprise and Business Critical perspectives.&lt;/p&gt;


&lt;h1&gt;
  
  
  Connectivity with Enterprise Edition
&lt;/h1&gt;

&lt;p&gt;Enterprise Edition does not support AWS PrivateLink, so connectivity to Snowflake is fundamentally internet-based.&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%2Fxtlvy8cqr13xdfmsimde.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%2Fxtlvy8cqr13xdfmsimde.png" width="800" height="355"&gt;&lt;/a&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Client / AWS Services
        ↓
    Internet
        ↓
    Snowflake
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Hearing “internet-based connectivity” may sound concerning at first.&lt;br&gt;
However, even with Enterprise Edition, it is possible to achieve a practical security level by combining multiple controls.&lt;/p&gt;


&lt;h2&gt;
  
  
  1. Restricting Source IPs with Network Policies
&lt;/h2&gt;

&lt;p&gt;Snowflake network policies can restrict allowed source IP addresses.&lt;/p&gt;

&lt;p&gt;By limiting access to known egress IPs such as:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Corporate network addresses&lt;/li&gt;
&lt;li&gt;Elastic IPs attached to NAT Gateways&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;you can significantly reduce the risk of unauthorized access.&lt;/p&gt;


&lt;h2&gt;
  
  
  2. Key Pair Authentication + AWS Secrets Manager
&lt;/h2&gt;

&lt;p&gt;For credential management, password authentication should generally be avoided in favor of key pair authentication.&lt;/p&gt;

&lt;p&gt;As of 2026, Snowflake is actively moving away from single-factor password authentication.&lt;br&gt;
For system integrations, Key Pair authentication or OAuth is now the recommended approach.&lt;/p&gt;

&lt;p&gt;When connecting from services such as AWS Lambda, storing private keys in AWS Secrets Manager is a practical approach.&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%2Fvf8cm97n1qdqrsjkag93.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%2Fvf8cm97n1qdqrsjkag93.png" width="800" height="420"&gt;&lt;/a&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Event Source
    ↓
AWS Lambda
    ↓
Secrets Manager
    ↓
Retrieve Private Key
    ↓
Snowflake (TLS over Internet)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I covered this approach in a previous article as well:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://dev.to/aws-builders/securely-implementing-snowflake-x-aws-lambda-integration-with-key-pair-authentication-secrets-2ba2"&gt;Securely Implementing Snowflake AWS Lambda Integration with Key Pair Authentication + Secrets Manager&lt;/a&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  3. TLS Encryption
&lt;/h2&gt;

&lt;p&gt;All communication with Snowflake is encrypted via TLS.&lt;/p&gt;

&lt;p&gt;This means the communication channel itself remains confidential.&lt;/p&gt;

&lt;p&gt;In other words, even with Enterprise Edition, combining:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;IP restrictions&lt;/li&gt;
&lt;li&gt;Key Pair authentication&lt;/li&gt;
&lt;li&gt;TLS encryption&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;can provide a practical level of security.&lt;/p&gt;

&lt;p&gt;However, traffic still traverses the public internet.&lt;/p&gt;




&lt;h2&gt;
  
  
  Limitations of Enterprise Edition
&lt;/h2&gt;

&lt;p&gt;Enterprise Edition cannot satisfy requirements such as:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Audit/compliance mandates requiring “no internet-based connectivity”&lt;/li&gt;
&lt;li&gt;Internal security policies mandating PrivateLink&lt;/li&gt;
&lt;li&gt;Handling regulated data under HIPAA or PCI DSS&lt;/li&gt;
&lt;li&gt;Encrypting data using customer-managed KMS keys (Tri-Secret Secure)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Once these requirements appear, Business Critical Edition or higher becomes necessary.&lt;/p&gt;




&lt;h1&gt;
  
  
  Connectivity with Business Critical Edition
&lt;/h1&gt;

&lt;p&gt;Business Critical Edition supports AWS PrivateLink, enabling private connectivity between AWS VPCs and Snowflake.&lt;/p&gt;

&lt;p&gt;In this architecture, traffic remains entirely within the AWS backbone network and never traverses the public internet.&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%2F6rn26lamu5y94bid1y99.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%2F6rn26lamu5y94bid1y99.png" width="800" height="355"&gt;&lt;/a&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;    AWS VPC
        ↓
VPC Interface Endpoint
        ↓
 AWS PrivateLink
        ↓
   Snowflake VPC
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  High-Level Setup Procedure
&lt;/h2&gt;

&lt;p&gt;Following the official documentation, the configuration can be summarized in several major steps.&lt;/p&gt;




&lt;h3&gt;
  
  
  1. Enable PrivateLink on the Snowflake Side
&lt;/h3&gt;

&lt;p&gt;Using the ACCOUNTADMIN role, authorize AWS PrivateLink for the Snowflake account.&lt;/p&gt;

&lt;p&gt;First, obtain a federation token using AWS STS.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;aws sts get-federation-token &lt;span class="nt"&gt;--name&lt;/span&gt; your-user-name
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then execute authorization from 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="n"&gt;USE&lt;/span&gt; &lt;span class="k"&gt;ROLE&lt;/span&gt; &lt;span class="n"&gt;ACCOUNTADMIN&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="k"&gt;SYSTEM&lt;/span&gt;&lt;span class="err"&gt;$&lt;/span&gt;&lt;span class="n"&gt;AUTHORIZE_PRIVATELINK&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="s1"&gt;'&amp;lt;aws_account_id&amp;gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="s1"&gt;'&amp;lt;federated_token&amp;gt;'&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 verify authorization with the following function.&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="k"&gt;SYSTEM&lt;/span&gt;&lt;span class="err"&gt;$&lt;/span&gt;&lt;span class="n"&gt;GET_PRIVATELINK&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'&amp;lt;aws_account_id&amp;gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'&amp;lt;federated_token&amp;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 the response returns:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Account is authorized for PrivateLink.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;then authorization succeeded.&lt;/p&gt;




&lt;h3&gt;
  
  
  2. Retrieve the VPC Endpoint ID
&lt;/h3&gt;

&lt;p&gt;Retrieve the information required for AWS VPC Endpoint creation.&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="k"&gt;SYSTEM&lt;/span&gt;&lt;span class="err"&gt;$&lt;/span&gt;&lt;span class="n"&gt;GET_PRIVATELINK_CONFIG&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Take note of the &lt;code&gt;privatelink-vpce-id&lt;/code&gt; value returned in the JSON response.&lt;/p&gt;

&lt;p&gt;This ID becomes the “service name” when creating the VPC Endpoint on AWS.&lt;/p&gt;




&lt;h3&gt;
  
  
  3. Create the VPC Endpoint on AWS
&lt;/h3&gt;

&lt;p&gt;Create an AWS VPC Interface Endpoint using the following configuration.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Service Name: the &lt;code&gt;privatelink-vpce-id&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;VPC: the source VPC&lt;/li&gt;
&lt;li&gt;Subnets: multi-AZ deployment is recommended&lt;/li&gt;
&lt;li&gt;Security Groups: allow ports 443 and 80 from Lambda/EC2&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Port 80 is required for OCSP (certificate revocation checking), so do not forget to allow it.&lt;/p&gt;




&lt;h3&gt;
  
  
  4. Configure DNS
&lt;/h3&gt;

&lt;p&gt;When using PrivateLink, the Snowflake account URL changes to the following format:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;&amp;lt;account_identifier&amp;gt;.privatelink.snowflakecomputing.com
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You must create a CNAME record mapping the endpoint returned by &lt;code&gt;SYSTEM$GET_PRIVATELINK_CONFIG()&lt;/code&gt; to the DNS name of the AWS VPC Endpoint.&lt;/p&gt;

&lt;p&gt;Using a Route 53 Private Hosted Zone is the most common approach.&lt;/p&gt;




&lt;h3&gt;
  
  
  5. Verify Connectivity
&lt;/h3&gt;

&lt;p&gt;Finally, verify connectivity from the client side.&lt;/p&gt;

&lt;p&gt;For diagnostics, the &lt;code&gt;SnowCD&lt;/code&gt; (Snowflake Connectivity Diagnostic Tool) is useful for validating PrivateLink connectivity.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;snowcd &amp;lt;hostfile&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  Configuring VPC Endpoints for S3 Access
&lt;/h2&gt;

&lt;p&gt;This is an easy detail to overlook.&lt;/p&gt;

&lt;p&gt;Snowflake drivers such as:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;JDBC&lt;/li&gt;
&lt;li&gt;ODBC&lt;/li&gt;
&lt;li&gt;Python Connector&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;internally access Amazon S3 during data load/unload operations against stages.&lt;/p&gt;

&lt;p&gt;Even if Snowflake connectivity itself is private via PrivateLink, S3 traffic may still traverse the public internet unless additional configuration is performed.&lt;/p&gt;

&lt;p&gt;Available approaches include:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Creating AWS VPC Interface Endpoints for Snowflake internal stages (recommended)&lt;/li&gt;
&lt;li&gt;Creating an S3 Gateway Endpoint to privatize S3 bucket access&lt;/li&gt;
&lt;li&gt;Allowing internet-based S3 access (strongly discouraged)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you want a fully private architecture, Snowflake officially recommends creating VPC Interface Endpoints for internal stages.&lt;/p&gt;




&lt;h2&gt;
  
  
  Blocking Public Access
&lt;/h2&gt;

&lt;p&gt;After establishing PrivateLink connectivity, you can also block public access from the Snowflake side.&lt;/p&gt;

&lt;p&gt;This allows only PrivateLink-based connectivity.&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="n"&gt;NETWORK&lt;/span&gt; &lt;span class="n"&gt;POLICY&lt;/span&gt; &lt;span class="n"&gt;privatelink_only&lt;/span&gt;
  &lt;span class="n"&gt;ALLOWED_IP_LIST&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'10.0.0.0/8'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="k"&gt;ALTER&lt;/span&gt; &lt;span class="n"&gt;ACCOUNT&lt;/span&gt; &lt;span class="k"&gt;SET&lt;/span&gt; &lt;span class="n"&gt;NETWORK_POLICY&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;privatelink_only&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Snowflake also provides:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;code&gt;SYSTEM$ENFORCE_PRIVATELINK_ACCESS_ONLY&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;“Enforce privatelink-only access”&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;which are also valid approaches.&lt;/p&gt;

&lt;p&gt;Combining VPN-based corporate IP ranges with PrivateLink-only access can create an even more secure architecture.&lt;/p&gt;




&lt;h2&gt;
  
  
  Leveraging Tri-Secret Secure
&lt;/h2&gt;

&lt;p&gt;Business Critical Edition also supports Tri-Secret Secure using AWS KMS customer-managed keys (CMKs).&lt;/p&gt;

&lt;p&gt;This mechanism requires both:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Snowflake-managed keys&lt;/li&gt;
&lt;li&gt;Customer-managed keys&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;as an AND condition for decryption.&lt;/p&gt;

&lt;p&gt;Even if Snowflake itself were compromised, data could not be decrypted without the customer-managed key.&lt;/p&gt;

&lt;p&gt;Combining:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;PrivateLink&lt;/li&gt;
&lt;li&gt;Tri-Secret Secure&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;creates a very strong architecture for regulatory compliance.&lt;/p&gt;

&lt;p&gt;I have not personally implemented this feature, so I will omit further details here.&lt;/p&gt;




&lt;h1&gt;
  
  
  Cross-Region Connectivity
&lt;/h1&gt;

&lt;p&gt;AWS PrivateLink is fundamentally designed for same-region connectivity.&lt;/p&gt;

&lt;p&gt;However, Business Critical Edition and above also support cross-region connectivity.&lt;/p&gt;

&lt;p&gt;For example:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Snowflake account in &lt;code&gt;US-EAST&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;AWS VPC in &lt;code&gt;AP-NORTHEAST-1 (Tokyo)&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;can still communicate privately via PrivateLink.&lt;/p&gt;

&lt;p&gt;That said, there are several caveats:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;PaaS services such as S3 and KMS do not support cross-region PrivateLink&lt;/li&gt;
&lt;li&gt;Government and China regions are not supported&lt;/li&gt;
&lt;li&gt;“Enable Cross Region Endpoint” must be enabled in the VPC console&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;In practice, aligning the Snowflake region with the application region generally results in a simpler and easier-to-operate architecture.&lt;/p&gt;

&lt;p&gt;Still, for globally distributed data platforms, these considerations become important.&lt;/p&gt;




&lt;h1&gt;
  
  
  Balancing Edition Selection and Cost
&lt;/h1&gt;

&lt;p&gt;Business Critical Edition provides major security advantages, but the credit cost is roughly 1.3x higher than Enterprise Edition.&lt;/p&gt;

&lt;p&gt;As rough on-demand reference pricing for US East in 2026:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Standard: approximately $2/credit&lt;/li&gt;
&lt;li&gt;Enterprise: approximately $3/credit&lt;/li&gt;
&lt;li&gt;Business Critical: approximately $4/credit&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you have a strict requirement that traffic must never traverse the public internet, Business Critical is effectively the only option.&lt;/p&gt;

&lt;p&gt;However, from a practical standpoint, balancing data sensitivity and cost often leads to architectures such as:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Production data warehouse (including sensitive data): Business Critical&lt;/li&gt;
&lt;li&gt;Development / testing environments: Enterprise&lt;/li&gt;
&lt;li&gt;Dedicated data sharing accounts: Enterprise&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Using multiple editions strategically within the same organization can also be a reasonable approach.&lt;/p&gt;




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

&lt;p&gt;In this article, including some reflections from my own experience, I introduced the differences between Snowflake editions and explored secure AWS connectivity using Business Critical Edition.&lt;/p&gt;

&lt;p&gt;The key points are:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Snowflake provides four editions (Standard / Enterprise / Business Critical / VPS), with higher editions adding stronger security and compliance capabilities&lt;/li&gt;
&lt;li&gt;AWS PrivateLink requires Business Critical Edition or higher, so networking/security requirements should be validated early&lt;/li&gt;
&lt;li&gt;Even Enterprise Edition can achieve reasonable security through network policies, Key Pair authentication, and TLS&lt;/li&gt;
&lt;li&gt;Business Critical enables private connectivity between AWS VPCs and Snowflake through PrivateLink, fully isolating traffic from the public internet&lt;/li&gt;
&lt;li&gt;S3 access must also be privatized, so VPC Endpoints for internal stages should be configured as well&lt;/li&gt;
&lt;li&gt;Combining Tri-Secret Secure with PrivateLink enables architectures well suited for regulatory compliance&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I think many teams struggle specifically with deciding between Enterprise Edition and Business Critical Edition.&lt;/p&gt;

&lt;p&gt;Although edition upgrades are possible later, they can significantly impact both architecture and cost.&lt;br&gt;
For that reason, it is best to organize these requirements carefully during the early stages of requirements definition and architecture design.&lt;/p&gt;

&lt;p&gt;I hope this article helps anyone looking to use Snowflake securely on AWS.&lt;/p&gt;

</description>
      <category>aws</category>
      <category>snowflake</category>
      <category>dataengineering</category>
    </item>
    <item>
      <title>What Is Apache Polaris? Why Open Data Catalogs Matter and How to Use Them with AWS</title>
      <dc:creator>Aki</dc:creator>
      <pubDate>Sat, 02 May 2026 06:27:16 +0000</pubDate>
      <link>https://dev.to/aws-builders/what-is-apache-polaris-why-open-data-catalogs-matter-and-how-to-use-them-with-aws-5gal</link>
      <guid>https://dev.to/aws-builders/what-is-apache-polaris-why-open-data-catalogs-matter-and-how-to-use-them-with-aws-5gal</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/28aa29c2f9fbeb" rel="noopener noreferrer"&gt;Apache Polarisとは何か？オープンなデータカタログが求められる理由とAWSとの組み合わせ方を整理する&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;/p&gt;

&lt;p&gt;In recent years, lakehouse architectures centered around Apache Iceberg have been rapidly expanding.&lt;/p&gt;

&lt;p&gt;By placing Iceberg tables on object storage such as S3, it has become possible to query the same data from multiple engines such as Athena, Snowflake, Spark, Trino, and Dremio.&lt;br&gt;
As a result, the discussion has shifted from &lt;em&gt;“Where should data be placed, and which engine should be used for analysis?”&lt;/em&gt; to &lt;em&gt;“Where should data ownership reside, and which catalog should be used to unify governance?”&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Amid this trend, &lt;strong&gt;Apache Polaris&lt;/strong&gt; has been attracting attention in recent years.&lt;br&gt;
Apache Polaris is an open-source implementation of the Iceberg REST Catalog, led by Snowflake and donated to the Apache Software Foundation.&lt;/p&gt;

&lt;p&gt;Multiple vendors—including Dremio, AWS, Google, Microsoft, and Confluent—are contributing to it, and it is positioned as an &lt;strong&gt;“open catalog”&lt;/strong&gt; that enables cross-platform management of Iceberg tables while avoiding vendor lock-in.&lt;/p&gt;

&lt;p&gt;In this article, I would like to think through the following:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;What Apache Polaris is&lt;/li&gt;
&lt;li&gt;Why open data catalogs are required&lt;/li&gt;
&lt;li&gt;Differences from AWS Glue Data Catalog&lt;/li&gt;
&lt;li&gt;Differences from Snowflake Horizon Catalog&lt;/li&gt;
&lt;li&gt;How responsibilities should be divided when combining with AWS&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;In conclusion, Apache Polaris is not something that &lt;em&gt;competes&lt;/em&gt; with AWS Glue Catalog or Snowflake Horizon Catalog; rather, they are catalogs that operate at different layers.&lt;/p&gt;

&lt;p&gt;It may be easier to understand Apache Polaris as a component that enables an architecture such as:&lt;br&gt;
&lt;strong&gt;“The data itself resides in AWS, the catalog is open, and analysis engines are selected based on use cases.”&lt;/strong&gt;&lt;/p&gt;


&lt;h2&gt;
  
  
  What is Apache Polaris?
&lt;/h2&gt;

&lt;p&gt;Apache Polaris is an open-source catalog implementation compliant with the Apache Iceberg REST Catalog specification.&lt;br&gt;
It was announced by Snowflake in 2024 and later became an incubation project under the Apache Software Foundation.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;The project has now graduated from incubation and has been promoted to a top-level Apache project.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Official site:&lt;br&gt;
&lt;a href="https://polaris.apache.org/" rel="noopener noreferrer"&gt;https://polaris.apache.org/&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;What Polaris aims to achieve is a &lt;strong&gt;common metadata and governance foundation in a lakehouse centered around Iceberg tables&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;A major characteristic is that it is not tied to any specific query engine or cloud vendor, and anyone can access it using the same specification via REST APIs.&lt;/p&gt;


&lt;h3&gt;
  
  
  Key Features of Apache Polaris
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Feature&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;Implementation of Iceberg REST Catalog&lt;/td&gt;
&lt;td&gt;Accessible via standardized REST APIs. Can be directly used from engines such as Spark, Trino, Flink, Snowflake, and Dremio&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Multi-catalog architecture&lt;/td&gt;
&lt;td&gt;Multiple catalogs can be defined within a single Polaris instance. Enables separation and management by team or business domain&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;RBAC (Role-Based Access Control)&lt;/td&gt;
&lt;td&gt;Provides a permission model combining principals, principal roles, and catalog roles&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;External catalog integration&lt;/td&gt;
&lt;td&gt;Can connect to other catalogs compliant with the Iceberg REST specification (e.g., Nessie, Gravitino)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;OSS / Managed support&lt;/td&gt;
&lt;td&gt;Can be self-hosted as OSS, or used as managed offerings such as Snowflake Open Catalog or Dremio Catalog&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;


&lt;h3&gt;
  
  
  What Apache Polaris Solves
&lt;/h3&gt;

&lt;p&gt;As Apache Iceberg has become more widely adopted, multiple Iceberg-compatible catalogs have emerged, including Hive Metastore, JDBC, Nessie, AWS Glue, and Snowflake.&lt;/p&gt;

&lt;p&gt;Since each has its own client libraries and interfaces, the following challenges have arisen:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The need to implement catalog clients for each programming language&lt;/li&gt;
&lt;li&gt;Inconsistent access control specifications across catalogs&lt;/li&gt;
&lt;li&gt;Difficulty enforcing governance across multiple catalogs&lt;/li&gt;
&lt;li&gt;As a result, the overall architecture becomes constrained by the chosen catalog&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;To solve these challenges, the Iceberg REST Catalog specification was introduced.&lt;br&gt;
Apache Polaris is an open-source implementation of that specification, further enhanced with multi-catalog support and RBAC.&lt;/p&gt;

&lt;p&gt;In other words, you can think of it as an &lt;strong&gt;open catalog for Apache Iceberg&lt;/strong&gt;.&lt;/p&gt;


&lt;h3&gt;
  
  
  Polaris Security Model
&lt;/h3&gt;

&lt;p&gt;The Polaris security model can be organized into the following three concepts:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Principal&lt;/strong&gt;: An entity representing a user or service. Accesses Polaris via client ID/secret, etc.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Principal Role&lt;/strong&gt;: A grouping of multiple catalog roles. Assigned to principals&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Catalog Role&lt;/strong&gt;: A set of permissions within a specific catalog. Includes permissions such as &lt;code&gt;TABLE_READ_DATA&lt;/code&gt;, &lt;code&gt;TABLE_CREATE&lt;/code&gt;, and &lt;code&gt;NAMESPACE_LIST&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For example, you can design it such that:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The &lt;code&gt;data_engineer&lt;/code&gt; principal role is assigned both &lt;em&gt;write access to prod_catalog&lt;/em&gt; and &lt;em&gt;administrative access to dev_catalog&lt;/em&gt;
&lt;/li&gt;
&lt;li&gt;The &lt;code&gt;data_analyst&lt;/code&gt; principal role is assigned only &lt;em&gt;read access to prod_catalog&lt;/em&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;An important point is that RBAC is centralized on the catalog side, eliminating the need to implement access control separately for each engine.&lt;/p&gt;


&lt;h2&gt;
  
  
  Why Open Data Catalogs Are Required
&lt;/h2&gt;

&lt;p&gt;Let us first consider why open data catalogs are required in the first place.&lt;/p&gt;


&lt;h3&gt;
  
  
  Separation of Data and Engines Has Become a Premise
&lt;/h3&gt;

&lt;p&gt;The greatest value of open table formats such as Apache Iceberg is the ability to separate data storage from query engines.&lt;/p&gt;

&lt;p&gt;It has become possible to freely choose engines such as Athena, Glue, Spark, Snowflake, Dremio, and DuckDB depending on the use case when querying Iceberg tables on S3.&lt;/p&gt;

&lt;p&gt;As a result, the key question in data platforms has shifted from &lt;em&gt;“Which product should we use?”&lt;/em&gt; to &lt;em&gt;“Where should data ownership reside, and who should be responsible for governance at which layer?”&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;However, while engines can now be freely selected, the remaining challenge is the &lt;strong&gt;catalog&lt;/strong&gt;.&lt;/p&gt;


&lt;h3&gt;
  
  
  What Happens When Catalogs Are Tied to Engines
&lt;/h3&gt;

&lt;p&gt;When using catalogs tightly coupled with query engines, the following situations tend to occur:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The data itself is open (S3 + Iceberg), but the catalog is tied to a specific engine&lt;/li&gt;
&lt;li&gt;You want to reference the same table from another engine, but the catalog does not support it&lt;/li&gt;
&lt;li&gt;Access control is fragmented across engines, making governance difficult&lt;/li&gt;
&lt;li&gt;Every time the catalog is changed, all engine-side configurations must be redone&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;In other words, even if storage and formats are open, a closed catalog significantly reduces the benefits of a lakehouse.&lt;/p&gt;

&lt;p&gt;Especially in today’s environments where multi-cloud, multiple products, and multiple engines are commonly combined, how to unify catalogs becomes a key challenge.&lt;/p&gt;


&lt;h3&gt;
  
  
  Requirements for an Open Catalog
&lt;/h3&gt;

&lt;p&gt;Based on this background, lakehouse catalogs are expected to meet the following requirements:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Requirement&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;Compliance with standard APIs&lt;/td&gt;
&lt;td&gt;Support vendor-neutral APIs such as the Iceberg REST Catalog specification&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Multi-engine support&lt;/td&gt;
&lt;td&gt;Usable across engines such as Spark, Trino, Flink, Snowflake, and Dremio&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Centralized RBAC&lt;/td&gt;
&lt;td&gt;Define permissions at the catalog level and apply consistent governance across all engines&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Multi-cloud / hybrid&lt;/td&gt;
&lt;td&gt;Not dependent on a specific cloud and capable of running on-premises when necessary&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;OSS sustainability&lt;/td&gt;
&lt;td&gt;Not discontinued based on vendor decisions; continuously developed in a community-driven manner&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Apache Polaris is a catalog designed to satisfy these requirements.&lt;/p&gt;


&lt;h2&gt;
  
  
  Differences from AWS Glue Data Catalog
&lt;/h2&gt;

&lt;p&gt;When building on AWS, AWS Glue Data Catalog is often positioned as the central data catalog.&lt;br&gt;
Here, we will organize the differences between AWS Glue Data Catalog and Apache Polaris.&lt;/p&gt;


&lt;h3&gt;
  
  
  Positioning of AWS Glue Data Catalog
&lt;/h3&gt;

&lt;p&gt;AWS Glue Data Catalog is a core metadata management service in AWS.&lt;/p&gt;

&lt;p&gt;It is natively integrated with AWS analytics services such as Athena, Glue, Redshift Spectrum, and EMR, and plays the role of managing data on S3 as a catalog.&lt;/p&gt;

&lt;p&gt;As discussed in previous articles, Glue Data Catalog is an excellent &lt;strong&gt;technical catalog used by data platforms&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;&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;/p&gt;


&lt;h3&gt;
  
  
  Functional Comparison
&lt;/h3&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;AWS Glue Data Catalog&lt;/th&gt;
&lt;th&gt;Apache Polaris&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Offering&lt;/td&gt;
&lt;td&gt;AWS-managed (closed)&lt;/td&gt;
&lt;td&gt;OSS / Managed (Snowflake Open Catalog, Dremio Catalog, etc.)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;API&lt;/td&gt;
&lt;td&gt;AWS proprietary API (recently also provides Iceberg REST compatibility)&lt;/td&gt;
&lt;td&gt;Iceberg REST Catalog specification (open)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Cloud support&lt;/td&gt;
&lt;td&gt;AWS&lt;/td&gt;
&lt;td&gt;Multi-cloud / on-prem&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Engines&lt;/td&gt;
&lt;td&gt;Athena, Glue, Redshift, EMR, Spark&lt;/td&gt;
&lt;td&gt;Spark, Trino, Flink, Snowflake, Dremio, StarRocks, DuckDB&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Multi-catalog&lt;/td&gt;
&lt;td&gt;Account-level (logical separation via Lake Formation)&lt;/td&gt;
&lt;td&gt;Native support for multiple catalogs within a single instance&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;Built-in RBAC (Principal / Principal Role / Catalog Role)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;External catalog integration&lt;/td&gt;
&lt;td&gt;Limited&lt;/td&gt;
&lt;td&gt;Can integrate with Iceberg REST-compliant catalogs (Nessie, Gravitino, etc.)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Non-Iceberg formats&lt;/td&gt;
&lt;td&gt;Supports Hive, JSON, CSV, Parquet, etc.&lt;/td&gt;
&lt;td&gt;Currently Iceberg-centric (Generic Table support on roadmap)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;


&lt;h3&gt;
  
  
  How to Interpret the Difference
&lt;/h3&gt;

&lt;p&gt;Rather than being in a competitive relationship, it is easier to understand them as catalogs with different roles.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;AWS Glue Data Catalog&lt;/strong&gt;: Strong integration with AWS services, making it the primary choice for workloads completed within AWS. It supports a wide range of data lake formats beyond Iceberg and features such as S3 crawling.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Apache Polaris&lt;/strong&gt;: A catalog that enables governance across multiple engines and clouds based on the industry-standard Iceberg REST API. It is effective when you want to enforce consistent RBAC across engines outside AWS (e.g., Snowflake, Dremio).&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;In summary:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;If your use case is &lt;strong&gt;AWS-contained and includes formats beyond Iceberg&lt;/strong&gt;, Glue Data Catalog is a practical choice&lt;/li&gt;
&lt;li&gt;If you want &lt;strong&gt;common management of Iceberg across multiple engines and a vendor-neutral catalog layer&lt;/strong&gt;, Polaris is suitable&lt;/li&gt;
&lt;/ul&gt;


&lt;h2&gt;
  
  
  Differences from Snowflake Horizon Catalog
&lt;/h2&gt;

&lt;p&gt;This is often confused, so let’s clarify the difference between Snowflake Horizon Catalog and Apache Polaris.&lt;br&gt;
Note that it is different from “Snowflake Open Catalog,” despite the similar name.&lt;/p&gt;


&lt;h3&gt;
  
  
  What is Snowflake Horizon Catalog?
&lt;/h3&gt;

&lt;p&gt;Snowflake Horizon Catalog is a data governance and discovery suite provided by Snowflake.&lt;/p&gt;

&lt;p&gt;For data managed within Snowflake (Snowflake-managed tables, stages, views, shared data, etc.), it provides:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Data discovery (search, tagging, descriptions)&lt;/li&gt;
&lt;li&gt;Lineage&lt;/li&gt;
&lt;li&gt;Data quality monitoring&lt;/li&gt;
&lt;li&gt;Masking policies and row access policies&lt;/li&gt;
&lt;li&gt;Automatic classification of sensitive data&lt;/li&gt;
&lt;li&gt;Compliance management&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;In terms of positioning, it is similar to Amazon DataZone + Lake Formation + Glue Data Quality in AWS.&lt;/p&gt;

&lt;p&gt;In other words, it is the layer responsible for &lt;strong&gt;cataloging and governance so that people can discover, understand, and trust data&lt;/strong&gt;.&lt;/p&gt;


&lt;h3&gt;
  
  
  What is Snowflake Open Catalog (Relation to Polaris)
&lt;/h3&gt;

&lt;p&gt;On the other hand, Snowflake Open Catalog is a managed offering of Apache Polaris.&lt;/p&gt;

&lt;p&gt;Although the name is confusing, this is the lakehouse catalog that serves as an Iceberg REST Catalog.&lt;/p&gt;

&lt;p&gt;In Snowflake’s model:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Snowflake Horizon Catalog: Business catalog and governance layer for Snowflake-managed data&lt;/li&gt;
&lt;li&gt;Snowflake Open Catalog (= Apache Polaris): Lakehouse catalog layer for open table formats such as Iceberg&lt;/li&gt;
&lt;/ul&gt;


&lt;h3&gt;
  
  
  Functional Comparison
&lt;/h3&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;Snowflake Horizon Catalog&lt;/th&gt;
&lt;th&gt;Apache Polaris&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Primary target&lt;/td&gt;
&lt;td&gt;Data in Snowflake (internal tables, shared data, etc.)&lt;/td&gt;
&lt;td&gt;Iceberg (Generic Table support for other formats is planned)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Layer&lt;/td&gt;
&lt;td&gt;Business catalog / governance layer&lt;/td&gt;
&lt;td&gt;Lakehouse catalog layer (technical catalog)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Offering&lt;/td&gt;
&lt;td&gt;Built into Snowflake (closed)&lt;/td&gt;
&lt;td&gt;OSS / Managed&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;API&lt;/td&gt;
&lt;td&gt;Snowflake proprietary&lt;/td&gt;
&lt;td&gt;Iceberg REST Catalog specification (open)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Data location&lt;/td&gt;
&lt;td&gt;Snowflake internal storage or recognized external data&lt;/td&gt;
&lt;td&gt;Iceberg tables on cloud storage&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Scope&lt;/td&gt;
&lt;td&gt;Within Snowflake organizations&lt;/td&gt;
&lt;td&gt;Across multiple engines and clouds&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;


&lt;h3&gt;
  
  
  How to Interpret the Difference
&lt;/h3&gt;

&lt;p&gt;Again, these are not in opposition but complementary.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Snowflake Horizon Catalog&lt;/strong&gt;: Upper layer that provides data to business users, handling discovery, quality, masking, etc.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Apache Polaris&lt;/strong&gt;: Lower layer (metadata foundation) that exposes Iceberg tables to multiple engines&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Conceptually, the structure 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;┌──────────────────────────────────────────────┐
│  Business Catalog / Governance Layer         │ ← Snowflake Horizon Catalog
│  (Discovery / Lineage / Quality / Masking)   │   Amazon DataZone, etc.
└─────────────────────┬────────────────────────┘
                      │
┌─────────────────────┴────────────────────────┐
│  Lakehouse Catalog Layer                     │ ← Apache Polaris
│  (Iceberg REST Catalog / RBAC)               │   AWS Glue Data Catalog, etc.
└─────────────────────┬────────────────────────┘
                      │
┌─────────────────────┴────────────────────────┐
│  Data Lake (S3 / GCS / Azure Blob)           │
│  Iceberg / Parquet                           │
└──────────────────────────────────────────────┘
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If you think of Snowflake Horizon Catalog and Apache Polaris as “choosing one or the other,” it feels unnatural, but when organized as different layers, the division of responsibilities becomes clear.&lt;/p&gt;




&lt;h2&gt;
  
  
  How to Combine with AWS
&lt;/h2&gt;

&lt;p&gt;From here, we will consider cases where Apache Polaris is introduced into an AWS environment.&lt;br&gt;
Since AWS already has a powerful catalog called Glue Data Catalog, it is important to clarify &lt;strong&gt;how Polaris should be positioned&lt;/strong&gt; and &lt;strong&gt;who is responsible for what&lt;/strong&gt;.&lt;/p&gt;


&lt;h3&gt;
  
  
  Expected Architecture
&lt;/h3&gt;

&lt;p&gt;Representative configurations can be organized into the following three patterns.&lt;/p&gt;


&lt;h4&gt;
  
  
  Pattern 1: AWS-only (Glue Data Catalog-centered)
&lt;/h4&gt;

&lt;p&gt;This is the simplest configuration.&lt;br&gt;
It is a typical setup using S3 + Iceberg + Glue Data Catalog, along with Athena / Glue / Redshift Spectrum.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Catalog: AWS Glue Data Catalog&lt;/li&gt;
&lt;li&gt;Governance: IAM + Lake Formation&lt;/li&gt;
&lt;li&gt;Query engines: Athena, Redshift Spectrum, Glue ETL, EMR&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If everything is completed within AWS and there is no strong need to share with external engines, this configuration remains the most practical.&lt;br&gt;
There is no need to forcibly introduce Apache Polaris.&lt;/p&gt;


&lt;h4&gt;
  
  
  Pattern 2: AWS + Snowflake (Using Polaris as a shared catalog foundation)
&lt;/h4&gt;

&lt;p&gt;This configuration is effective when you want to reference the same Iceberg tables from both AWS (e.g., Athena) and Snowflake.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Data storage: S3 + Iceberg&lt;/li&gt;
&lt;li&gt;Catalog: Apache Polaris (OSS self-hosted or Snowflake Open Catalog)&lt;/li&gt;
&lt;li&gt;AWS side: Reference Polaris as an Iceberg REST Catalog (via Spark or third-party tools)&lt;/li&gt;
&lt;li&gt;Snowflake side: Connect to Polaris using External Volume and Catalog Integration (&lt;code&gt;CATALOG_SOURCE = POLARIS&lt;/code&gt;)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;From the Snowflake side, Polaris can be referenced directly as follows:&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;polaris_catalog_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;POLARIS&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://&amp;lt;polaris-host&amp;gt;/api/catalog'&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;'&amp;lt;your_polaris_catalog&amp;gt;'&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;OAUTH&lt;/span&gt;
    &lt;span class="n"&gt;OAUTH_CLIENT_ID&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'&amp;lt;polaris_client_id&amp;gt;'&lt;/span&gt;
    &lt;span class="n"&gt;OAUTH_CLIENT_SECRET&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'&amp;lt;polaris_client_secret&amp;gt;'&lt;/span&gt;
    &lt;span class="n"&gt;OAUTH_ALLOWED_SCOPES&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'PRINCIPAL_ROLE:ALL'&lt;/span&gt;&lt;span class="p"&gt;)&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;h4&gt;
  
  
  Pattern 3: Multi-engine / multi-cloud configuration
&lt;/h4&gt;

&lt;p&gt;In addition to Snowflake, this configuration includes multiple engines such as Dremio, Databricks, Trino, and Flink.&lt;/p&gt;

&lt;p&gt;In this case, all engines reference Polaris as a common Iceberg REST Catalog.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Data storage: S3 (and other cloud storage if needed)&lt;/li&gt;
&lt;li&gt;Catalog: Apache Polaris (center of governance)&lt;/li&gt;
&lt;li&gt;Query engines: Snowflake, Dremio, Spark, Trino, Flink, etc.&lt;/li&gt;
&lt;li&gt;Governance: Polaris provides unified RBAC across all engines&lt;/li&gt;
&lt;/ul&gt;




&lt;h3&gt;
  
  
  How to Think About Responsibility Separation
&lt;/h3&gt;

&lt;p&gt;This is the key point.&lt;br&gt;
When combining Polaris, AWS, Snowflake, and others, it is important to clearly define &lt;strong&gt;who is responsible for which layer&lt;/strong&gt;.&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;Primary Owner&lt;/th&gt;
&lt;th&gt;Notes&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Data storage (files)&lt;/td&gt;
&lt;td&gt;AWS (S3)&lt;/td&gt;
&lt;td&gt;Storage location of the data. Single Source of Truth&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Storage access control&lt;/td&gt;
&lt;td&gt;AWS (IAM)&lt;/td&gt;
&lt;td&gt;Access permissions to S3 buckets/prefixes are defined on the AWS side&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Table metadata&lt;/td&gt;
&lt;td&gt;Apache Polaris&lt;/td&gt;
&lt;td&gt;Source of Truth for Iceberg metadata such as schema, snapshots, partitions&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Table-level RBAC&lt;/td&gt;
&lt;td&gt;Apache Polaris&lt;/td&gt;
&lt;td&gt;Applies consistent permission rules across engines&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;ETL / pipelines&lt;/td&gt;
&lt;td&gt;AWS Glue / Lambda / EMR / Spark&lt;/td&gt;
&lt;td&gt;Responsible for ingestion and transformation&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Query execution&lt;/td&gt;
&lt;td&gt;Athena / Snowflake / Dremio / Spark&lt;/td&gt;
&lt;td&gt;Engines selected based on use case&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Business catalog / discovery&lt;/td&gt;
&lt;td&gt;Snowflake Horizon Catalog / Amazon DataZone&lt;/td&gt;
&lt;td&gt;Higher-layer features for search, lineage, quality for users&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Data quality&lt;/td&gt;
&lt;td&gt;Glue Data Quality / Snowflake DMF&lt;/td&gt;
&lt;td&gt;Implemented at engine or quality service layer&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;What is especially important is the &lt;strong&gt;three-layer separation&lt;/strong&gt;:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Data resides in AWS, the catalog is Polaris, and usage is handled by each engine&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;By making this separation explicit:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;AWS can focus on storage and IAM management&lt;/li&gt;
&lt;li&gt;Polaris can focus on metadata and access control&lt;/li&gt;
&lt;li&gt;Each query engine can focus on its strengths&lt;/li&gt;
&lt;/ul&gt;




&lt;h3&gt;
  
  
  Considerations When Adopting Polaris
&lt;/h3&gt;

&lt;p&gt;Polaris is powerful, but there are also important considerations:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Operational cost when self-hosting OSS&lt;/strong&gt;: Running on EKS or EC2 requires a metastore (e.g., PostgreSQL), authentication infrastructure, monitoring, and upgrade handling&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Managed services are often more practical&lt;/strong&gt;: Using Snowflake Open Catalog or Dremio Catalog significantly reduces operational burden&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Less seamless integration with AWS services compared to Glue&lt;/strong&gt;: For AWS-native services such as Athena, Redshift, and QuickSight, using Glue Data Catalog is far more straightforward&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Need to avoid double governance&lt;/strong&gt;: If IAM policies on S3 and RBAC in Polaris are inconsistent, troubleshooting becomes complex&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;In other words, when deciding whether to adopt Apache Polaris in an AWS environment, it is realistic to evaluate based on:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Whether multi-engine requirements exist&lt;/li&gt;
&lt;li&gt;The organization’s stance on vendor lock-in&lt;/li&gt;
&lt;li&gt;Whether operational cost is acceptable (or managed services can be used)&lt;/li&gt;
&lt;/ul&gt;




&lt;h3&gt;
  
  
  A Practical Approach
&lt;/h3&gt;

&lt;p&gt;Personally, when considering Polaris in an AWS environment, the following phased approach is practical:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Build a lakehouse within AWS using Glue Data Catalog + Iceberg&lt;/li&gt;
&lt;li&gt;When integration with other engines such as Snowflake becomes necessary, consider introducing an Iceberg REST layer&lt;/li&gt;
&lt;li&gt;At that point, compare “Glue Iceberg REST endpoint,” “Apache Polaris OSS,” and “Snowflake Open Catalog” based on requirements&lt;/li&gt;
&lt;li&gt;If multi-engine / multi-cloud requirements become clear, redesign with Polaris (especially managed) at the center&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Rather than designing with Polaris from the beginning, it is often more practical to &lt;strong&gt;replace the catalog layer with an open one when requirements mature&lt;/strong&gt;.&lt;/p&gt;




&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;In this article, we organized the key points around Apache Polaris.&lt;/p&gt;

&lt;p&gt;In the world of data platforms, while storage and formats have become open, a closed catalog reduces the benefits of a lakehouse by half.&lt;/p&gt;

&lt;p&gt;Therefore, there is a need for an &lt;strong&gt;open catalog&lt;/strong&gt; that complies with the Iceberg REST Catalog specification and enables unified governance across multiple engines and clouds.&lt;br&gt;
Apache Polaris is designed to fulfill exactly that role.&lt;/p&gt;

&lt;p&gt;However, it is important to think not in terms of “which one to choose” among Polaris, AWS Glue Data Catalog, and Snowflake Horizon Catalog, but rather &lt;strong&gt;which layer each is responsible for&lt;/strong&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;AWS Glue Data Catalog: Technical catalog within AWS (still the primary choice for AWS-only workloads)&lt;/li&gt;
&lt;li&gt;Apache Polaris: Lakehouse catalog centered on Iceberg, shared across multiple engines&lt;/li&gt;
&lt;li&gt;Snowflake Horizon Catalog: Business catalog and governance layer for Snowflake users&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Even when combining with AWS, by consciously separating responsibilities as&lt;br&gt;
&lt;strong&gt;“data in AWS, catalog in Polaris, analytics in engines, business catalog in another layer”&lt;/strong&gt;,&lt;br&gt;
you can design an architecture that leverages the strengths of each.&lt;/p&gt;

&lt;p&gt;Going forward, lakehouse architectures are expected to increasingly adopt vendor-neutral designs.&lt;br&gt;
Apache Polaris is likely to become an important component supporting that openness.&lt;/p&gt;

&lt;p&gt;I hope this article will be helpful for those considering Apache Polaris or designing lakehouse architectures across multiple platforms such as AWS and Snowflake.&lt;/p&gt;

</description>
      <category>aws</category>
      <category>snowflake</category>
      <category>dataengineering</category>
      <category>iceberg</category>
    </item>
    <item>
      <title>Lightweight ETL on AWS Lambda Using DuckDB and Snowflake Connector</title>
      <dc:creator>Aki</dc:creator>
      <pubDate>Sat, 04 Apr 2026 13:54:10 +0000</pubDate>
      <link>https://dev.to/aws-builders/lightweight-etl-on-aws-lambda-using-duckdb-and-snowflake-connector-3h19</link>
      <guid>https://dev.to/aws-builders/lightweight-etl-on-aws-lambda-using-duckdb-and-snowflake-connector-3h19</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/b47f2bf700b68b" rel="noopener noreferrer"&gt;AWS Lambda × DuckDB × Snowflake ConnectorによるETLの実装&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;/p&gt;

&lt;p&gt;In my previous article, I introduced how to connect to Snowflake from AWS Lambda using Key Pair authentication.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://dev.to/aws-builders/securely-implementing-snowflake-x-aws-lambda-integration-with-key-pair-authentication-secrets-2ba2"&gt;Securely Implementing Snowflake AWS Lambda Integration with Key Pair Authentication + Secrets Manager&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;This time, I would like to try the event-driven data ingestion approach that I introduced in the previous article.&lt;/p&gt;

&lt;p&gt;In this article, I will implement an event-driven ETL pipeline that uses DuckDB on AWS Lambda to perform lightweight transformations on Parquet files stored in Amazon S3 and then load the processed data into Snowflake.&lt;/p&gt;

&lt;p&gt;In addition, during the implementation process, I encountered an interesting limitation where &lt;code&gt;write_pandas&lt;/code&gt; fails when writing to a Catalog-Linked Database. I will also summarize the root cause and the workaround.&lt;/p&gt;




&lt;h1&gt;
  
  
  Why Snowpipe Is Not Enough
&lt;/h1&gt;

&lt;p&gt;Snowpipe is a very convenient feature for automatic data ingestion.&lt;/p&gt;

&lt;p&gt;However, it has limitations when it comes to data transformation and complex filtering.&lt;/p&gt;

&lt;p&gt;In other words, when you need preprocessing, filtering, or the integration of multiple events, you need to choose another approach.&lt;/p&gt;

&lt;p&gt;In such cases, AWS Lambda becomes a strong option due to its high flexibility.&lt;/p&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.amazonaws.com%2Fuploads%2Farticles%2Feunw8xz2jgbd4jm9vqp6.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%2Feunw8xz2jgbd4jm9vqp6.png" width="800" height="247"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;A Parquet file is uploaded to S3&lt;/li&gt;
&lt;li&gt;Lambda is triggered by the S3 event&lt;/li&gt;
&lt;li&gt;DuckDB reads the data and performs the required transformations&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;snowflake.connector&lt;/code&gt; writes the data into Snowflake&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The two key libraries used in this implementation are shown below.&lt;/p&gt;




&lt;h2&gt;
  
  
  DuckDB
&lt;/h2&gt;

&lt;p&gt;DuckDB is an embedded database engine designed for OLAP (Online Analytical Processing).&lt;/p&gt;

&lt;p&gt;Because DuckDB is extremely lightweight and supports in-memory processing, it can run efficiently even in a simple execution environment such as AWS Lambda.&lt;/p&gt;

&lt;p&gt;It is said to provide particularly strong performance for batch workloads such as data analytics and ETL processing.&lt;/p&gt;

&lt;p&gt;In addition, it enables SQL-based filtering and lightweight data transformations, allowing for intuitive implementations.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://duckdb.org/" rel="noopener noreferrer"&gt;https://duckdb.org/&lt;/a&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  Snowflake Connector
&lt;/h2&gt;

&lt;p&gt;Snowflake Connector for Python is a library that provides an interface for connecting to Snowflake and executing all standard operations.&lt;/p&gt;

&lt;p&gt;By using this library, it becomes possible to operate Snowflake from runtime environments such as Lambda.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://docs.snowflake.com/en/developer-guide/python-connector/python-connector" rel="noopener noreferrer"&gt;https://docs.snowflake.com/en/developer-guide/python-connector/python-connector&lt;/a&gt;&lt;/p&gt;




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

&lt;p&gt;In the sample code below, &lt;code&gt;WHERE VendorID = 1&lt;/code&gt; is added as an ETL filter.&lt;/p&gt;

&lt;p&gt;By performing filtering and data transformation inside Lambda, highly flexible preprocessing becomes possible.&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;duckdb&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;boto3&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;snowflake.connector&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;cryptography.hazmat.primitives&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;serialization&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;cryptography.hazmat.backends&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;default_backend&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;snowflake.connector.pandas_tools&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;write_pandas&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;time&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;random&lt;/span&gt;

&lt;span class="n"&gt;SECRET_ID&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;snowflake-keypair&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;


&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;get_secret&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
    &lt;span class="n"&gt;client&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;boto3&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;client&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;secretsmanager&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get_secret_value&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;SecretId&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;SECRET_ID&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;json&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;loads&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;SecretString&lt;/span&gt;&lt;span class="sh"&gt;"&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;lambda_handler&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;event&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;conn&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;
    &lt;span class="n"&gt;duckdb_connection&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;

    &lt;span class="k"&gt;try&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;duckdb_connection&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;duckdb&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;connect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;database&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;:memory:&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;duckdb_connection&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;execute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;SET home_directory=&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;/tmp&lt;/span&gt;&lt;span class="sh"&gt;'"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;duckdb_connection&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;execute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;INSTALL httpfs&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;duckdb_connection&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;execute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;LOAD httpfs&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

        &lt;span class="n"&gt;s3_bucket&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;event&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Records&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;][&lt;/span&gt;&lt;span class="mi"&gt;0&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&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;bucket&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="n"&gt;s3_object_key&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;event&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Records&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;][&lt;/span&gt;&lt;span class="mi"&gt;0&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&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;object&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;key&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;

        &lt;span class="n"&gt;s3_input_path&lt;/span&gt; &lt;span class="o"&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;s3://&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;s3_bucket&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;/&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;s3_object_key&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&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;S3 input path: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;s3_input_path&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="n"&gt;query&lt;/span&gt; &lt;span class="o"&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;
            SELECT *
            FROM read_parquet(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;s3_input_path&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;)
            WHERE VendorID = 1
        &lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;

        &lt;span class="n"&gt;result_arrow_table&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;duckdb_connection&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;execute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;query&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;fetch_arrow_table&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;DuckDB filtered rows: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;result_arrow_table&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;num_rows&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="n"&gt;secret&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;get_secret&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

        &lt;span class="n"&gt;private_key_obj&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;serialization&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;load_pem_private_key&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="n"&gt;secret&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;privateKey&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nf"&gt;encode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;utf-8&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
            &lt;span class="n"&gt;password&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;backend&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nf"&gt;default_backend&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="p"&gt;)&lt;/span&gt;

        &lt;span class="n"&gt;conn&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;snowflake&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;connector&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;connect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="n"&gt;user&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;secret&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&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
            &lt;span class="n"&gt;account&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;secret&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;account&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
            &lt;span class="n"&gt;private_key&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;private_key_obj&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;role&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;secret&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="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;role&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
            &lt;span class="n"&gt;warehouse&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;secret&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="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;warehouse&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
            &lt;span class="n"&gt;database&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;secret&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="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;database&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
            &lt;span class="n"&gt;schema&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;secret&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="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;schema&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;cur&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;conn&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;cursor&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="n"&gt;cur&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;execute&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;USE DATABASE &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;secret&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;database&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&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="n"&gt;cur&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;execute&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;USE SCHEMA &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;secret&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;schema&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&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="n"&gt;cur&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;close&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

        &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;pandas&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;pd&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;result_arrow_table&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;to_pandas&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;tpep_pickup_datetime&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="n"&gt;pd&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;to_datetime&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;tpep_pickup_datetime&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
            &lt;span class="n"&gt;unit&lt;/span&gt;&lt;span class="o"&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="n"&gt;dt&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;strftime&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;%Y-%m-%d %H:%M:%S&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="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;tpep_dropoff_datetime&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="n"&gt;pd&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;to_datetime&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;tpep_dropoff_datetime&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
            &lt;span class="n"&gt;unit&lt;/span&gt;&lt;span class="o"&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="n"&gt;dt&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;strftime&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;%Y-%m-%d %H:%M:%S&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

        &lt;span class="n"&gt;success&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;nchunks&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;nrows&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;_&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;write_pandas&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="n"&gt;conn&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="n"&gt;table_name&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;YELLOW_TRIPDATA&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;Snowflake write success=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;success&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="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;rows=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;nrows&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;, chunks=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;nchunks&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="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;statusCode&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;200&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;body&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="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Processed &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;result_arrow_table&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;num_rows&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; rows &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;and wrote &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;nrows&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; rows to Snowflake.&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;except&lt;/span&gt; &lt;span class="nb"&gt;Exception&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;e&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;An error occurred: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;e&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="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;traceback&lt;/span&gt;
        &lt;span class="n"&gt;traceback&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;print_exc&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;statusCode&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;500&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;body&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;str&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;finally&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;conn&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;conn&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;close&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;duckdb_connection&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;duckdb_connection&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;close&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h1&gt;
  
  
  Execution Result
&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.amazonaws.com%2Fuploads%2Farticles%2Fackggvtaz462vtl750tf.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%2Fackggvtaz462vtl750tf.png" width="800" height="202"&gt;&lt;/a&gt;&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%2F5gu8zxnvcnh4st7xdle5.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%2F5gu8zxnvcnh4st7xdle5.png" width="800" height="418"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;As shown above, the data was successfully written.&lt;/p&gt;




&lt;h1&gt;
  
  
  Switching the Destination to a Catalog-Linked Database
&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.amazonaws.com%2Fuploads%2Farticles%2Fj7g7qkaznukvw61kefhr.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%2Fj7g7qkaznukvw61kefhr.png" width="800" height="351"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;As introduced in a previous article, what happens if we try writing to a table configured with a Catalog-Linked Database (Iceberg)?&lt;/p&gt;

&lt;p&gt;Let’s test it.&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;
  
  
  A Write Error Occurs
&lt;/h2&gt;

&lt;p&gt;When attempting to write to the Catalog-Linked Database, the following error occurred:&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="o"&gt;{&lt;/span&gt;
  &lt;span class="s2"&gt;"statusCode"&lt;/span&gt;: 500,
  &lt;span class="s2"&gt;"body"&lt;/span&gt;: &lt;span class="s2"&gt;"093678 (0A000): SQL Compilation Error: This operation is not supported in a catalog-linked database."&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  Why Writing to a Catalog-Linked Database Fails
&lt;/h2&gt;

&lt;p&gt;The reason this happens is due to the interaction between &lt;code&gt;write_pandas&lt;/code&gt; in the Snowflake Connector and the constraints of a Catalog-Linked Database.&lt;/p&gt;

&lt;p&gt;Internally, &lt;code&gt;write_pandas&lt;/code&gt; creates a temporary stage.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://docs.snowflake.com/en/developer-guide/python-connector/python-connector-api#id13" rel="noopener noreferrer"&gt;Python Connector API&lt;/a&gt;&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Writes a pandas DataFrame to a table in a Snowflake database.&lt;br&gt;
To write the data to the table, the function saves the data to Parquet files, uses the PUT command to upload these files to a temporary stage, and uses the COPY INTO &amp;lt;table&amp;gt; command to copy the data from the files to the table.&lt;/p&gt;

&lt;p&gt;You can use some of the function parameters to control how the PUT and COPY INTO &amp;lt;table&amp;gt; statements are executed.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;However, stage creation is not supported in a Catalog-Linked Database.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://docs.snowflake.com/en/user-guide/tables-iceberg-catalog-linked-database#considerations-for-using-a-catalog-linked-database-for-iceberg-tables" rel="noopener noreferrer"&gt;Considerations for using a catalog-linked database for Iceberg tables&lt;/a&gt;&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;You can create schemas, externally managed Iceberg tables, or database roles in a catalog-linked database. Creating other Snowflake objects isn't currently supported.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;This conflict causes &lt;code&gt;write_pandas&lt;/code&gt; to fail with the error:&lt;/p&gt;

&lt;p&gt;&lt;code&gt;This operation is not supported in a catalog-linked database.&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;More specifically, the temporary stage created internally falls under the category of “other Snowflake objects,” so the error occurs at the point where &lt;code&gt;CREATE TEMPORARY STAGE&lt;/code&gt; is executed.&lt;/p&gt;

&lt;p&gt;That said, there is a workaround.&lt;/p&gt;




&lt;h2&gt;
  
  
  How to Write to a Catalog-Linked Database
&lt;/h2&gt;

&lt;p&gt;A relatively simple approach is to use an &lt;code&gt;INSERT&lt;/code&gt; statement directly.&lt;/p&gt;

&lt;p&gt;Here is an example implementation:&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;for&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nf"&gt;range&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&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;df&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="mi"&gt;1000&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; 
            &lt;span class="n"&gt;chunk&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="n"&gt;iloc&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="o"&gt;+&lt;/span&gt;&lt;span class="mi"&gt;1000&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
            &lt;span class="n"&gt;columns&lt;/span&gt; &lt;span class="o"&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="nf"&gt;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;chunk&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;columns&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="n"&gt;placeholders&lt;/span&gt; &lt;span class="o"&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="nf"&gt;join&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;%s&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="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;chunk&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;columns&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
            &lt;span class="n"&gt;sql&lt;/span&gt; &lt;span class="o"&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;INSERT INTO &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;secret&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;schema&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;.YELLOW_TRIPDATA (&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;columns&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;) VALUES (&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;placeholders&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="n"&gt;cur&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;executemany&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;sql&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;chunk&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;values&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;tolist&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Alternatively, another good approach is to create a stage in a different database and execute the &lt;code&gt;INSERT&lt;/code&gt; through that route.&lt;/p&gt;




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

&lt;p&gt;In this article, I implemented a lightweight event-driven ETL pipeline triggered by S3 events using AWS Lambda, DuckDB, and the Snowflake Connector.&lt;/p&gt;

&lt;p&gt;By using DuckDB inside Lambda, I was able to perform SQL-based filtering and lightweight transformations directly on Parquet files stored in S3, and successfully load the processed results into Snowflake.&lt;/p&gt;

&lt;p&gt;In addition, I confirmed an important limitation: when using &lt;code&gt;write_pandas&lt;/code&gt; against a Catalog-Linked Database (Iceberg), the write fails because the connector internally creates a temporary stage.&lt;/p&gt;

&lt;p&gt;Although there are some constraints, combining DuckDB and the Snowflake Connector enables the construction of a low-cost and flexible data processing pipeline for Snowflake.&lt;/p&gt;

&lt;p&gt;The key point is to clearly understand how Snowflake manages Iceberg tables.&lt;/p&gt;

&lt;p&gt;It is important to determine whether the table is a Snowflake-managed Iceberg table or connected through mechanisms such as a Catalog-Linked Database, and to properly understand that structure.&lt;/p&gt;

&lt;p&gt;In any case, the combination of Snowflake and Iceberg is an extremely powerful option for building a Lakehouse architecture.&lt;/p&gt;

&lt;p&gt;I hope this article will be helpful for those considering lightweight data processing and real-time ETL pipelines with AWS and Snowflake when working with Iceberg tables.&lt;/p&gt;




</description>
      <category>aws</category>
      <category>snowflake</category>
      <category>dataengineering</category>
    </item>
  </channel>
</rss>
