Snowflake → AWS Catalog Federation via LakeFormation
Create a table in Snowflake → it automatically appears in AWS (Glue/LakeFormation) → queryable from Athena/SageMaker without any manual registration.
In this post, we show you how to connect the AWS Glue Data Catalog to Snowflake Horizon Catalog and query the data using AWS analytics services. We cover how to set up catalogs in Horizon Catalog and configure required permissions, create and configure the federation connection in AWS Glue , implement fine-grained access controls using AWS Lake Formation, and finally, query federated tables using Amazon Athena . This step-by-step approach guides you through the complete process of establishing a integration between your Snowflake and AWS data environments.
Step 1: Create S3 Bucket (AWS)
AWS Console → S3 → Create bucket:
- Name:
sf-federation-demo-1111222333 - Region: us-west-2
Step 2: Create IAM Role for External Volume (AWS)
AWS Console → IAM → Roles → Create role:
- Trusted entity: Custom trust policy
- Paste this placeholder (we'll update it in Step 5):
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
{
"Version": "2012-10-17",
"Statement": [{
"Effect": "Allow",
"Principal": {
"AWS": "arn:aws:iam::444455556666:user/xxxx-x-xxxxxxxx"
},
"Action": "sts:AssumeRole",
"Condition": {
"StringEquals": {
"sts:ExternalId": "PLACEHOLDER"
}
}
}]
}
- Role name:
sf-federation-external-volume-role - After creation, add an inline policy named
s3-readwrite:
1
2
3
4
5
6
7
8
9
10
11
{
"Version": "2012-10-17",
"Statement": [{
"Effect": "Allow",
"Action": ["s3:GetObject", "s3:PutObject", "s3:DeleteObject", "s3:ListBucket", "s3:GetBucketLocation"],
"Resource": [
"arn:aws:s3:::sf-federation-demo-111122223333",
"arn:aws:s3:::sf-federation-demo-111122223333/*"
]
}]
}
Step 3: Create External Volume (Snowflake)
1
2
3
4
5
6
7
8
CREATE OR REPLACE EXTERNAL VOLUME sf_iceberg_volume
STORAGE_LOCATIONS = ((
NAME = 'sf-s3-storage'
STORAGE_PROVIDER = 'S3'
STORAGE_BASE_URL = 's3://sf-federation-demo-111122223333/'
STORAGE_AWS_ROLE_ARN = 'arn:aws:iam::111122223333:role/sf-federation-external-volume-role'
))
ALLOW_WRITES = TRUE;
Step 4: Get External ID (Snowflake)
1
DESCRIBE EXTERNAL VOLUME sf_iceberg_volume;
Note down:
STORAGE_AWS_IAM_USER_ARN(e.g. arn:aws:iam::444455556666:user/xxxx-x-xxxxxxxx)STORAGE_AWS_EXTERNAL_ID(e.g.AWSPARTNER_SFCRole=2_xxxxx=)
Step 5: Update IAM Role Trust Policy with Real External ID (AWS)
AWS Console → IAM → Roles →
sf-federation-external-volume-role → Trust relationships → Edit:Replace the placeholder with real values from Step 4:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
{
"Version": "2012-10-17",
"Statement": [{
"Effect": "Allow",
"Principal": {
"AWS": "<STORAGE_AWS_IAM_USER_ARN from Step 4>"
},
"Action": "sts:AssumeRole",
"Condition": {
"StringEquals": {
"sts:ExternalId": "<STORAGE_AWS_EXTERNAL_ID from Step 4>"
}
}
}]
}
⏱ Wait 30 seconds for IAM propagation before proceeding.
Step 6: Create Database, Schema, and Iceberg Table (Snowflake)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
CREATE DATABASE IF NOT EXISTS SF_FEDERATION_DEMO;
CREATE SCHEMA IF NOT EXISTS SF_FEDERATION_DEMO.PUBLIC;
-- IMPORTANT: Athena does not support Iceberg v3. Force v2.
ALTER ACCOUNT SET ICEBERG_VERSION_DEFAULT = 2;
CREATE OR REPLACE ICEBERG TABLE SF_FEDERATION_DEMO.PUBLIC.customer_complaints (
complaint_id INT,
customer_name STRING,
complaint_type STRING,
severity STRING,
status STRING,
created_at TIMESTAMP
)
CATALOG = 'SNOWFLAKE'
EXTERNAL_VOLUME = 'sf_iceberg_volume'
BASE_LOCATION = 'customer_complaints/';
INSERT INTO SF_FEDERATION_DEMO.PUBLIC.customer_complaints VALUES
(1, 'Alice Johnson', 'Billing', 'High', 'Open', '2026-06-01 10:00:00'),
(2, 'Bob Smith', 'Service', 'Medium', 'Resolved', '2026-06-10 14:30:00'),
(3, 'Carol Williams', 'Product', 'Low', 'In Progress', '2026-06-15 09:00:00');
-- Verify data is written to S3
SELECT * FROM SF_FEDERATION_DEMO.PUBLIC.customer_complaints;
If you getError assuming AWS_ROLE→ Step 5 trust policy hasn't propagated. Wait 30s and retry.
Step 7: Configure Access Control (Snowflake)
1
2
3
4
5
6
CREATE OR REPLACE ROLE sf_federation_role;
GRANT USAGE ON DATABASE SF_FEDERATION_DEMO TO ROLE sf_federation_role;
GRANT USAGE ON SCHEMA SF_FEDERATION_DEMO.PUBLIC TO ROLE sf_federation_role;
GRANT SELECT ON ALL TABLES IN SCHEMA SF_FEDERATION_DEMO.PUBLIC TO ROLE sf_federation_role;
GRANT SELECT ON FUTURE ICEBERG TABLES IN SCHEMA SF_FEDERATION_DEMO.PUBLIC TO ROLE sf_federation_role;
GRANT ROLE sf_federation_role TO ROLE ACCOUNTADMIN;
Step 8: Create Service User and PAT (Snowflake)
1
2
3
4
5
6
7
CREATE OR REPLACE USER horizon_rest_srv TYPE = SERVICE DEFAULT_ROLE = sf_federation_role;
GRANT ROLE sf_federation_role TO USER horizon_rest_srv;
ALTER USER horizon_rest_srv ADD PROGRAMMATIC ACCESS TOKEN horizon_rest_pat
DAYS_TO_EXPIRY = 30
ROLE_RESTRICTION = 'SF_FEDERATION_ROLE'
COMMENT = 'PAT for AWS Glue catalog federation';
Copy the PAT value from the output. You need it in the next step.
Step 9: Exchange PAT for Access Token (Terminal)
1
2
3
4
5
curl -s -X POST 'https://your-snowflake-account.snowflakecomputing.com/polaris/api/catalog/v1/oauth/tokens' \
--header 'Content-Type: application/x-www-form-urlencoded' \
--data-urlencode 'grant_type=client_credentials' \
--data-urlencode 'scope=session:role:SF_FEDERATION_ROLE' \
--data-urlencode 'client_secret=<YOUR_PAT_VALUE>'
The response looks like:
1
{"access_token":"eyJraWQi...long string...Qog","token_type":"bearer","expires_in":3600}
Copy ONLY the value between the quotes after
"access_token":" — the long eyJ... string. Do NOT copy access_token": or any other part of the JSON.Token expires in 1 hour. For production, automate refresh with a scheduled Lambda (see end of doc).
Step 10: Store Bearer Token in Secrets Manager (AWS)
Option A — Console:
AWS Console → Secrets Manager →
sf-federation/horizon-token → Retrieve secret value → Edit:- Key:
BEARER_TOKEN - Value: Paste ONLY the
eyJ...token string (no quotes, no newlines, single line) - Click Save
If creating for the first time: Store a new secret → Other type of secret → Key:
BEARER_TOKEN, Value: the token → Name: sf-federation/horizon-tokenOption B — CloudShell (recommended to avoid newline issues):
⚠️ IMPORTANT: The token must be on a SINGLE LINE. Do not let the command wrap across multiple lines. Paste the entire command as one line, or use the method below:
1
2
3
4
5
6
7
8
# First, store your token in a variable (paste token after the = with no spaces)
TOKEN="eyJraWQi...paste your full token here...Qog"
# Then update the secret using the variable
aws secretsmanager put-secret-value \
--secret-id sf-federation/horizon-token \
--secret-string "{\"BEARER_TOKEN\":\"$TOKEN\"}" \
--region us-west-2
⚠️ Key MUST beBEARER_TOKEN— this is the reserved key for Custom authentication.
⚠️ The token value must have NO newlines, NO spaces, NO line breaks. If CloudShell wraps the display, that's fine — but the actual value must be continuous.
Step 11: Create IAM Role for LakeFormation (AWS)
AWS Console → IAM → Roles → Create role:
- Trusted entity: Custom trust policy:
1
2
3
4
5
6
7
8
9
10
{
"Version": "2012-10-17",
"Statement": [{
"Effect": "Allow",
"Principal": {
"Service": ["glue.amazonaws.com", "lakeformation.amazonaws.com"]
},
"Action": "sts:AssumeRole"
}]
}
- Role name:
sf-federation-lf-role - Add inline policy named
federation-access:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": ["secretsmanager:GetSecretValue", "secretsmanager:DescribeSecret", "secretsmanager:PutSecretValue"],
"Resource": ["arn:aws:secretsmanager:us-west-2:11112223333:secret:sf-federation/*"]
},
{
"Effect": "Allow",
"Action": ["s3:GetObject", "s3:ListBucket", "s3:GetBucketLocation"],
"Resource": [
"arn:aws:s3:::sf-federation-demo-11112223333",
"arn:aws:s3:::sf-federation-demo-11112223333/*"
]
}
]
}
Step 12: Create Federated Catalog (AWS — LakeFormation Console)
AWS Console → LakeFormation → Catalogs → Create Catalog
- Data source: Select Snowflake Horizon Catalog
- Catalog details:
- Catalog name:
sf-federation-demo - Catalog name in Snowflake:
SF_FEDERATION_DEMO(must match exact Snowflake database name, UPPERCASE)
- Catalog name:
- Connection details: Select New connection configurations
- Connection name:
sf-horizon-federation-connection - Workspace URL:
https://YOUR-SNOWFLAKE-ACCOUNT.snowflakecomputing.com - Casing type: Uppercase only
- Authentication type: Custom
- Secret: Select
sf-federation/horizon-tokenfrom dropdown
- Connection name:
- Registration:
- IAM Role: Select
sf-federation-lf-role
- IAM Role: Select
- Click Test Connection
- If successful → green checkmark
- If fails → check token validity (1hr expiry) and role permissions
- Click Create Catalog
Step 13: Grant LakeFormation Permissions (AWS)
AWS Console → LakeFormation → Data permissions → Grant:
- Principal: Your IAM user/role (whoever will query from Athena)
- Resource: Catalog →
sf-federation-demo - Permissions: ALL
Step 14: Test from Athena (AWS)
Open Athena → Select data source AwsDataCatalog → select catalog
sf-federation-demo:1
SELECT * FROM "sf-federation-demo"."PUBLIC"."CUSTOMER_COMPLAINTS" LIMIT 10;
⚠️ Use UPPERCASE for schema and table names (Snowflake stores them in uppercase).
Step 15: Verify Auto-Discovery (Snowflake + Athena) ✅ CONFIRMED
Create a new table in Snowflake:
1
2
3
4
5
6
7
8
9
10
11
12
13
CREATE OR REPLACE ICEBERG TABLE SF_FEDERATION_DEMO.PUBLIC.service_orders (
order_id INT,
customer_name STRING,
service STRING,
amount FLOAT
)
CATALOG = 'SNOWFLAKE'
EXTERNAL_VOLUME = 'sf_iceberg_volume'
BASE_LOCATION = 'service_orders/';
INSERT INTO SF_FEDERATION_DEMO.PUBLIC.service_orders VALUES
(1, 'Dave Brown', 'Premium Support', 499.99),
(2, 'Eve Clark', 'Installation', 299.99);
Then in Athena — no manual registration needed:
1
SELECT * FROM "sf-federation-demo"."PUBLIC"."SERVICE_ORDERS" LIMIT 10;
Troubleshooting
| Error | Cause | Fix |
|---|---|---|
| Error assuming AWS_ROLE in Snowflake | Trust policy wrong or missing External ID | DESCRIBE EXTERNAL VOLUME → update trust policy in Step 5 → wait 30s |
| invalid_scope on curl token exchange | Role name wrong (must be UPPERCASE) or Horizon not enabled | Use uppercase role name. If still fails, contact Snowflake support. |
| ALLOW_WRITES error in Snowflake | External volume set to read-only | Already set to TRUE in Step 3. If not: ALTER EXTERNAL VOLUME sf_iceberg_volume SET ALLOW_WRITES = TRUE; |
| Test Connection fails in LF console | Token expired (1hr) or secret key wrong | Re-run Step 9, update secret value in Secrets Manager, retry |
| Query hangs in Athena | LakeFormation permission not granted | Step 13 — grant ALL on the federated catalog to your principal |
| Table not visible | Casing mismatch | Use UPPERCASE in Athena queries: "PUBLIC"."TABLE_NAME" |
| Workspace URL error in console | Missing or extra suffix | Use just https://account.snowflakecomputing.com — console adds the rest |
Token Refresh (for production)
The PAT-exchanged bearer token expires in 1 hour. For production use:
- Create a Lambda function that:
- Reads the PAT from Secrets Manager (store PAT as a separate secret)
- Calls the Polaris token endpoint
- Updates the
BEARER_TOKENinsf-federation/horizon-token
- Schedule it via EventBridge every 50 minutes
- The Glue connection always reads the latest secret value
Cleanup
AWS Console:
- LakeFormation → Catalogs → Delete
sf-federation-demo - LakeFormation → Data locations → Deregister the connection
- Glue → Connections → Delete
sf-horizon-federation-connection - IAM → Roles → Delete
sf-federation-lf-roleandsf-federation-external-volume-role - Secrets Manager → Delete
sf-federation/horizon-token - S3 → Empty and delete
sf-federation-demo-11122223333
Snowflake:
1
2
3
4
5
6
7
DROP TABLE IF EXISTS SF_FEDERATION_DEMO.PUBLIC.customer_complaints;
DROP TABLE IF EXISTS SF_FEDERATION_DEMO.PUBLIC.service_orders;
DROP SCHEMA IF EXISTS SF_FEDERATION_DEMO.PUBLIC;
DROP DATABASE IF EXISTS SF_FEDERATION_DEMO;
DROP EXTERNAL VOLUME IF EXISTS sf_iceberg_volume;
DROP USER IF EXISTS horizon_rest_srv;
DROP ROLE IF EXISTS sf_federation_role;Enjoyed reading this content? Let the author know!
Your likes, comments, shares, and saves help creators reach more builders.
Loading recommendations
Loading article