---
name: setup-tundri-local-dev
description: Configure your local environment to run tundri against a client's Snowflake account. Use when setting up .env credentials, uv virtual environment, running dry run or production run of tundri against Snowflake locally.
disable-model-invocation: true
---

# Set Up Local Development for Tundri

Configure your local environment to run tundri against a client's Snowflake account. Use this skill after the repository and Snowflake account are set up and key pairs have been generated.

## Context

Tundri reads `permifrost.yml` and applies the declared state to Snowflake. To run it locally, you need:

- A Python environment with tundri installed (managed via `uv`)
- A `.env` file with Snowflake connection credentials
- The `permifrost` user's private key file on your local machine

This setup is for local development and testing. Production runs happen through CI/CD (see [setup-tundri-cicd](../setup-tundri-cicd/)).

## Prerequisites

- The `<client>-snowflake` repository cloned locally (via [setup-tundri-repository](../setup-tundri-repository/))
- The Snowflake account created and `permifrost` user bootstrapped (via [setup-tundri-snowflake-account](../setup-tundri-snowflake-account/))
- `uv` installed ([installation guide](https://docs.astral.sh/uv/getting-started/installation/))
- `op` CLI installed and signed in (see [1password skill](../../../../../security/1password/))
- The `permifrost` user's private key (`.p8` file) — either still on your local machine from [snowflake-generate-keypair](../snowflake-generate-keypair/), or retrieve it from 1Password using `op` (see step 2 below)
- The Snowflake account identifier (found in Snowflake UI under Settings > Account)

## Steps

**Checklist:**

- [ ] 1. Create a virtual environment and install dependencies
- [ ] 2. Configure the `.env` file
- [ ] 3. Run a dry run to validate
- [ ] 4. Run a production run to apply changes

### 1. Create a virtual environment and install dependencies

Navigate to the root of the `<client>-snowflake` repository (where `permifrost.yml` lives):

```bash
cd <client>-snowflake

# Create a virtual environment and install dependencies in one step
uv venv
uv pip install -r requirements.txt
```

### 2. Configure the `.env` file

Copy the example file:

```bash
cp .env.example .env
```

**Retrieve credentials from 1Password using `op` CLI** (see [1password skill](../../../../../security/1password/) for full reference):

First, download the private key file if you no longer have it locally:

```bash
op read --out-file ./<client>_permifrost_snowflake.p8 \
  "op://<client_vault>/<Client Name> - Snowflake: permifrost/private_key"
```

Then read the passphrase and account identifier:

```bash
# Read the passphrase
op read "op://<client_vault>/<Client Name> - Snowflake: permifrost/passphrase"

# Read the Snowflake account identifier (stored in the admin entry)
op read "op://<client_vault>/<Client Name> - Snowflake: admin/account_identifier"
```

**Alternative: Use `op inject` to populate the `.env` file automatically.**

Create a `.env.tpl` template (or modify `.env.example`) with `op://` references:

```
PERMISSION_BOT_USER=PERMIFROST
PERMISSION_BOT_ACCOUNT={{ op://<client_vault>/<Client Name> - Snowflake: admin/account_identifier }}
PERMISSION_BOT_WAREHOUSE=ADMIN
PERMISSION_BOT_KEY_PATH='./<client>_permifrost_snowflake.p8'
PERMISSION_BOT_KEY_PASSPHRASE='{{ op://<client_vault>/<Client Name> - Snowflake: permifrost/passphrase }}'
PERMISSION_BOT_DATABASE=PERMIFROST
PERMISSION_BOT_ROLE=SECURITYADMIN
```

Then inject the real values:

```bash
op inject -i .env.tpl -o .env
```

This writes the `.env` file with actual secrets substituted, without exposing them in shell history.

**Where to get each value:**

| Variable | Source |
|---|---|
| `PERMISSION_BOT_ACCOUNT` | 1Password: `op read "op://<client_vault>/<Client Name> - Snowflake: admin/account_identifier"` — or Snowflake UI > Settings > Account > "Account Identifier" |
| `PERMISSION_BOT_KEY_PATH` | Local path to the `.p8` private key — download from 1Password via `op read --out-file` if needed |
| `PERMISSION_BOT_KEY_PASSPHRASE` | 1Password: `op read "op://<client_vault>/<Client Name> - Snowflake: permifrost/passphrase"` |

**How to find the account identifier (if not stored in 1Password yet):**

1. In the Snowflake UI, open Settings (bottom-left corner)
2. Go to "Account"
3. If there are multiple accounts, select the one you're working with
4. Click "View account details"
5. Copy the value of "Account Identifier" (e.g. `VNLCRUD-SANDBOX`)

<img src="references/account-identifier-step1.png" alt="Snowflake settings → Account menu" width="50%" />
<img src="references/account-identifier-step2.png" alt="Account details page showing Account Identifier field" width="50%" />

**Important:** The account identifier format varies by cloud provider:
- **AWS:** `<org>-<account>` (e.g. `VNLCRUD-SANDBOX`)
- **Azure:** `<org>.<region>.azure` (e.g. `abc134.west-europe.azure`)
- **GCP:** `<org>.<region>.gcp` (e.g. `abc134.europe-west4.gcp`)

**Important:** Keep the `.env` file in the same directory as `permifrost.yml`. Tundri automatically loads environment variables from it.

### 3. Run a dry run to validate

A dry run shows what SQL commands tundri *would* execute without actually applying them:

```bash
uv run tundri run --filepath permifrost.yml --dry
```

**If the dry run fails with `Insufficient privileges to operate on user '<NAME>'`:** your client's bootstrap admin user is named something other than `admin` and is therefore not in tundri's default skip list. Pass `--users-to-skip <admin_username>` (and use the same flag in step 4 below + both CI workflow files):

```bash
uv run tundri run --filepath permifrost.yml --dry --users-to-skip <admin_username>
```

The flag *replaces* the default skip list (`[admin, snowflake, auto_dba]`), so include `snowflake` if you still want it skipped.

Review the output to ensure the planned changes look correct. Common things to check:
- No unexpected `DROP` statements
- `CREATE` statements match what you expect
- `GRANT` and `REVOKE` statements are correct

### 4. Run a production run to apply changes

Once the dry run looks good, apply the changes:

```bash
uv run tundri run --filepath permifrost.yml
```

If you used `--users-to-skip <admin_username>` in step 3, append the same flag here:

```bash
uv run tundri run --filepath permifrost.yml --users-to-skip <admin_username>
```

**Warning:** This applies changes directly to Snowflake, even when running locally. Only run this when you're confident the `permifrost.yml` is correct.

## Validation

- [ ] `uv run tundri run --filepath permifrost.yml --dry` completes without errors
- [ ] The dry run output shows expected CREATE/GRANT statements (no unexpected DROPs)
- [ ] `uv run tundri run --filepath permifrost.yml` applies changes successfully
- [ ] You can log in to Snowflake and verify the created objects (databases, warehouses, roles, users)

## Troubleshooting

### Error: Warehouse 'COMPUTE_WH' does not exist or not authorized

The default Snowflake warehouse was not dropped during account setup. Go back to [setup-tundri-snowflake-account](../setup-tundri-snowflake-account/) and run the DROP command.

### Error: JWT token is invalid

Mismatch between the private key file and the public key registered in Snowflake. Verify:
1. The `PERMISSION_BOT_KEY_PATH` in `.env` points to the correct `.p8` file (generated in [snowflake-generate-keypair](../snowflake-generate-keypair/))
2. The `PERMISSION_BOT_KEY_PASSPHRASE` matches the passphrase used when generating the key
3. The public key in `permifrost.yml` matches the generated `.pub` file
4. The public key set in Snowflake via `ALTER USER` (in [setup-tundri-snowflake-account](../setup-tundri-snowflake-account/) step 4) matches

If needed, re-register the public key in Snowflake:

```sql
USE ROLE securityadmin;
ALTER USER permifrost SET rsa_public_key = '<correct_public_key>';
```

### Error: Password empty

Check that the `.env` file is in the same directory as `permifrost.yml` and that all values are properly quoted.

## Next steps

After validating locally:
1. Generate key pairs for remaining service users (airflow, dbt, etc.) using [snowflake-generate-keypair](../snowflake-generate-keypair/) and add their public keys to `permifrost.yml`
2. [setup-tundri-cicd](../setup-tundri-cicd/) — configure GitHub Actions to automate tundri runs on PRs and merges
