> ## Documentation Index
> Fetch the complete documentation index at: https://docs.streamnative.io/llms.txt
> Use this file to discover all available pages before exploring further.

# Horizon Catalog for Iceberg on AWS

This guide describes how to prepare a Snowflake Horizon Catalog for use with StreamNative Ursa as an Iceberg catalog on AWS.

For background, see the Snowflake documentation: [Write to Apache Iceberg tables using an external query engine through Snowflake Horizon Catalog](https://docs.snowflake.com/en/LIMITEDACCESS/iceberg/tables-iceberg-write-using-external-write-engine-snowflake-horizon).

## Prerequisites

* A Snowflake account with Horizon Catalog enabled
* An AWS account with permissions to create S3 buckets and IAM roles
* The `ACCOUNTADMIN` role in Snowflake (required for several SQL operations below)

## 1. Create an External Volume

The Horizon Catalog uses a Snowflake **External Volume** to access object storage. Reference: [Tutorial: Create your first Apache Iceberg table](https://docs.snowflake.com/en/user-guide/tutorials/create-your-first-iceberg-table#create-a-table).

### 1.1 Identify the Snowflake Account Region

Find the region of your Snowflake account so the S3 bucket can be created in the same region.

<img src="https://mintcdn.com/streamnative/IhGBhfNZfZHreuAr/images/ursa-lakehouse/horizon-aws-01.webp?fit=max&auto=format&n=IhGBhfNZfZHreuAr&q=85&s=e55c40237d2519fa3df53dfa089aa925" alt="Snowflake region" width="1485" height="1920" data-path="images/ursa-lakehouse/horizon-aws-01.webp" />

### 1.2 Create an S3 Bucket

Create an S3 bucket in the AWS console, in the same region as your Snowflake account.

<img src="https://mintcdn.com/streamnative/IhGBhfNZfZHreuAr/images/ursa-lakehouse/horizon-aws-02.webp?fit=max&auto=format&n=IhGBhfNZfZHreuAr&q=85&s=df108319a3d6058e1aabeb172511dc18" alt="Create S3 bucket" width="1920" height="609" data-path="images/ursa-lakehouse/horizon-aws-02.webp" />

### 1.3 Create an IAM Policy

Create an IAM policy granting access to the bucket. Replace `<your-bucket>` with the bucket name from step 1.2.

```json theme={null}
{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Action": [
        "s3:PutObject",
        "s3:GetObject",
        "s3:GetObjectVersion",
        "s3:DeleteObject",
        "s3:DeleteObjectVersion"
      ],
      "Resource": "arn:aws:s3:::<your-bucket>/*"
    },
    {
      "Effect": "Allow",
      "Action": [
        "s3:ListBucket",
        "s3:GetBucketLocation"
      ],
      "Resource": "arn:aws:s3:::<your-bucket>",
      "Condition": {
        "StringLike": {
          "s3:prefix": ["*"]
        }
      }
    }
  ]
}
```

<img src="https://mintcdn.com/streamnative/IhGBhfNZfZHreuAr/images/ursa-lakehouse/horizon-aws-03.webp?fit=max&auto=format&n=IhGBhfNZfZHreuAr&q=85&s=f8bc9cf6a765bd9466ab48c2a96564f2" alt="IAM policy" width="1920" height="966" data-path="images/ursa-lakehouse/horizon-aws-03.webp" />

### 1.4 Create an IAM Role

Create an IAM role with an External ID (for example, `iceberg_table_external_id`). The trust policy will be updated in step 1.6.

<img src="https://mintcdn.com/streamnative/IhGBhfNZfZHreuAr/images/ursa-lakehouse/horizon-aws-04.webp?fit=max&auto=format&n=IhGBhfNZfZHreuAr&q=85&s=e6afbcbcc582819a8565e9dba8bca662" alt="Create IAM role" width="1920" height="972" data-path="images/ursa-lakehouse/horizon-aws-04.webp" />

Attach the policy from step 1.3 to the role.

<img src="https://mintcdn.com/streamnative/IhGBhfNZfZHreuAr/images/ursa-lakehouse/horizon-aws-05.webp?fit=max&auto=format&n=IhGBhfNZfZHreuAr&q=85&s=0afb0359d37b09435c7cca5ef2383dea" alt="Bind policy to role" width="1920" height="952" data-path="images/ursa-lakehouse/horizon-aws-05.webp" />

### 1.5 Create the External Volume in Snowflake

Switch to the `ACCOUNTADMIN` role and run the following SQL, substituting your values:

```sql theme={null}
CREATE OR REPLACE EXTERNAL VOLUME <volume_name>
   STORAGE_LOCATIONS = (
      (
         NAME = '<location-name>'
         STORAGE_PROVIDER = 'S3'
         STORAGE_BASE_URL = 's3://<your-bucket>/'
         STORAGE_AWS_ROLE_ARN = 'arn:aws:iam::<your-account-id>:role/<your-role-name>'
         STORAGE_AWS_EXTERNAL_ID = '<your-external-id>'
      )
   )
   ALLOW_WRITES = TRUE;
```

<img src="https://mintcdn.com/streamnative/IhGBhfNZfZHreuAr/images/ursa-lakehouse/horizon-aws-06.webp?fit=max&auto=format&n=IhGBhfNZfZHreuAr&q=85&s=b58dad1393a7e848eb6353ba0261db6e" alt="External volume" width="1920" height="728" data-path="images/ursa-lakehouse/horizon-aws-06.webp" />

If the command fails with a permission error, ensure you are using the `ACCOUNTADMIN` role:

<img src="https://mintcdn.com/streamnative/IhGBhfNZfZHreuAr/images/ursa-lakehouse/horizon-aws-07.webp?fit=max&auto=format&n=IhGBhfNZfZHreuAr&q=85&s=e3818cf4e42ff402c27b3335308b0d7b" alt="Permission issue" width="1920" height="1385" data-path="images/ursa-lakehouse/horizon-aws-07.webp" />

<img src="https://mintcdn.com/streamnative/IhGBhfNZfZHreuAr/images/ursa-lakehouse/horizon-aws-08.webp?fit=max&auto=format&n=IhGBhfNZfZHreuAr&q=85&s=26f6466a04a8ebdfc0d64ac0f316b994" alt="Switch to ACCOUNTADMIN" width="1920" height="1129" data-path="images/ursa-lakehouse/horizon-aws-08.webp" />

### 1.6 Configure the Trust Relationship

After the external volume is created, retrieve the Snowflake-generated IAM user ARN:

```sql theme={null}
DESC EXTERNAL VOLUME <volume_name>;
```

The `STORAGE_AWS_IAM_USER_ARN` field contains the value (for example, `arn:aws:iam::<snowflake-account>:user/<id>`).

<img src="https://mintcdn.com/streamnative/IhGBhfNZfZHreuAr/images/ursa-lakehouse/horizon-aws-09.webp?fit=max&auto=format&n=IhGBhfNZfZHreuAr&q=85&s=37580a1ce37941ad404d961ecee30ac0" alt="Volume info" width="1920" height="1536" data-path="images/ursa-lakehouse/horizon-aws-09.webp" />

Return to the AWS IAM console and update the trust policy of the role created in step 1.4 to allow `STORAGE_AWS_IAM_USER_ARN` to assume the role.

<img src="https://mintcdn.com/streamnative/IhGBhfNZfZHreuAr/images/ursa-lakehouse/horizon-aws-10.webp?fit=max&auto=format&n=IhGBhfNZfZHreuAr&q=85&s=f13b0a488791e329739182523f93e3ee" alt="Update trust policy" width="1920" height="929" data-path="images/ursa-lakehouse/horizon-aws-10.webp" />

<img src="https://mintcdn.com/streamnative/IhGBhfNZfZHreuAr/images/ursa-lakehouse/horizon-aws-11.webp?fit=max&auto=format&n=IhGBhfNZfZHreuAr&q=85&s=ab0c700726fdae6c4fbe03dd1d2349cf" alt="Update trust policy" width="1920" height="971" data-path="images/ursa-lakehouse/horizon-aws-11.webp" />

## 2. Configure Access Control

> **Note:** If you already have roles configured with access to the Iceberg tables you want to use, you can skip this section.

For details, see [Configuring access control](https://docs.snowflake.com/en/user-guide/security-access-control-configure).

```sql theme={null}
GRANT ROLE ACCOUNTADMIN, SYSADMIN TO USER <username>;
```

<img src="https://mintcdn.com/streamnative/IhGBhfNZfZHreuAr/images/ursa-lakehouse/horizon-aws-12.webp?fit=max&auto=format&n=IhGBhfNZfZHreuAr&q=85&s=579fb5f2910181a25f34839c72771c5e" alt="Grant roles" width="1920" height="613" data-path="images/ursa-lakehouse/horizon-aws-12.webp" />

## 3. Create the Catalog Database

Switch to `ACCOUNTADMIN` and create the catalog database in the Snowflake UI. Then bind the database to the external volume:

```sql theme={null}
ALTER DATABASE <db_name> SET CATALOG = 'SNOWFLAKE';
ALTER DATABASE <db_name> SET EXTERNAL_VOLUME = '<volume_name>';
```

<img src="https://mintcdn.com/streamnative/IhGBhfNZfZHreuAr/images/ursa-lakehouse/horizon-aws-13.webp?fit=max&auto=format&n=IhGBhfNZfZHreuAr&q=85&s=d4f4d70610c0eff346dff687930bdc25" alt="Create catalog" width="1920" height="1054" data-path="images/ursa-lakehouse/horizon-aws-13.webp" />

## 4. Obtain an Access Token

Snowflake Horizon supports three authentication methods: External OAuth, Key-pair authentication, and Programmatic Access Token (PAT). This guide uses **PAT**.

### 4.1 Create an Authentication Policy

Switch to `ACCOUNTADMIN` and run:

```sql theme={null}
USE <db_name>;

CREATE AUTHENTICATION POLICY <policy_name>
  PAT_POLICY=(NETWORK_POLICY_EVALUATION = ENFORCED_NOT_REQUIRED);

ALTER ACCOUNT SET AUTHENTICATION POLICY <policy_name>;

ALTER AUTHENTICATION POLICY <policy_name>
  SET AUTHENTICATION_METHODS = ('OAUTH', 'PASSWORD', 'PROGRAMMATIC_ACCESS_TOKEN');

ALTER AUTHENTICATION POLICY <policy_name>
  SET PAT_POLICY = (MAX_EXPIRY_IN_DAYS=365, DEFAULT_EXPIRY_IN_DAYS=365);

ALTER USER IF EXISTS <username> ADD PROGRAMMATIC ACCESS TOKEN <token_name>
  DAYS_TO_EXPIRY = 365
  COMMENT = 'PAT for StreamNative Ursa';
```

Record the generated PAT.

<img src="https://mintcdn.com/streamnative/IhGBhfNZfZHreuAr/images/ursa-lakehouse/horizon-aws-14.webp?fit=max&auto=format&n=IhGBhfNZfZHreuAr&q=85&s=a12c8cac284dcb2bafb1c34dab1c5521" alt="Generate PAT" width="1920" height="557" data-path="images/ursa-lakehouse/horizon-aws-14.webp" />

### 4.2 Verify the Endpoint

Generate an OAuth access token using the PAT and verify the Horizon REST endpoint:

```bash theme={null}
curl -i --fail -X POST \
  "https://<account_identifier>.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:<role>' \
  --data-urlencode 'client_secret=<PAT>'

curl -i --fail -X GET \
  "https://<account_identifier>.snowflakecomputing.com/polaris/api/catalog/v1/config?warehouse=<database_name>" \
  -H "Authorization: Bearer <access_token>" \
  -H "Content-Type: application/json"
```

### 4.3 Grant Permissions

```sql theme={null}
GRANT ROLE PUBLIC TO USER <username>;
GRANT USAGE ON DATABASE <db_name> TO ROLE PUBLIC;
GRANT USAGE ON EXTERNAL VOLUME <volume_name> TO ROLE PUBLIC;
```

## Catalog Information Summary

When the steps above are complete, collect the following values for the StreamNative Ursa compaction service:

```sql theme={null}
-- Determine the URI prefix
SELECT CURRENT_ORGANIZATION_NAME() || '-' || CURRENT_ACCOUNT_NAME();
```

| Value                | Description                                                                   |
| -------------------- | ----------------------------------------------------------------------------- |
| `iceberg.uri`        | `https://<organization>-<account>.snowflakecomputing.com/polaris/api/catalog` |
| `iceberg.warehouse`  | The catalog database name created in step 3                                   |
| `iceberg.credential` | The PAT generated in step 4.1                                                 |
| `iceberg.scope`      | `session:role:<role>` (e.g., `session:role:PUBLIC`)                           |

For the next steps, see [Configure Lakehouse Catalogs](../../../configure-lakehouse-catalogs).
