Example: Onboard Snowflake with Terraform, key pair stored by Monte Carlo
Connect Snowflake through your Monte Carlo cloud deployment with Terraform. The private key is read from a file on your machine into a write-only argument, so it never appears in the chat, the plan or the Terraform state.
In this example Monte Carlo connects to Snowflake directly from its cloud, and Monte Carlo stores the key pair. There's no agent and nothing to deploy in your cloud. Claude writes the Terraform; you run it on your machine, where Terraform reads the private key from a file and sends it straight to Monte Carlo.
| Who | What | How |
|---|---|---|
| Claude | Read your account, pick the cloud deployment, write the Terraform | MCP tools, then files in your working directory |
| You | Create the credentials, warehouse and connection | terraform apply |
| Claude | Validate the connection | validate_connection, get_validation_run |
For credentials that stay in your own secret store instead, see Onboard Snowflake through a new AWS agent, all with Terraform.
Before you start
- Claude with the Agent Toolkit plugin, signed in to the Monte Carlo MCP server. See Agent Toolkit.
- Terraform 1.11 or later (write-only arguments), with the Monte Carlo provider (
monte-carlo-data/montecarlo) authenticated through your Monte Carlo CLI profile.montecarlo whoamishows which account it reaches. - A Snowflake key pair: the public key set on the Snowflake user Monte Carlo logs in as, and the private key as a PEM file on your machine. If the private key is encrypted, have its passphrase ready for Terraform (never for Claude).
- The user's grants from Snowflake, including
IMPORTED PRIVILEGESon theSNOWFLAKEdatabase for query logs. - Network: Monte Carlo connects from its cloud, so a Snowflake network policy, if you have one, must allow Monte Carlo's IP addresses.
1. Ask Claude
/monte-carlo-onboarding connect Snowflake on our Monte Carlo cloud deployment with Terraform; Monte Carlo can store the key pair, the private key is at <path-to-key.p8>
Claude starts by saying it won't open the key file: anything a tool reads ends up in the chat and its logs, so the path only goes into the Terraform you run. It then reads your account, picks the enabled cloud deployment (nothing new is provisioned), and asks only for what it can't discover:
- the Snowflake account identifier (
SELECT CURRENT_ACCOUNT(), CURRENT_REGION();); - the user the public key is set on (another user fails with
JWT token is invalid); - the Snowflake warehouse Monte Carlo queries with (without one, queries fail with "No active warehouse selected");
- whether the private key is encrypted;
- names for the warehouse and the connection.
2. Answer Claude's questions
- Snowflake account: <account-identifier>
- User: <snowflake-user> (the public key is set on it)
- Warehouse: <snowflake-warehouse>
- The key is not encrypted.
- Warehouse name: <warehouse-name>. Connection name: <connection-name>.
- The grants script ran for this user and warehouse.
3. The Terraform Claude writes
No variable has a default, and terraform.tfvars holds only non-secret values: the key's path, never its contents.
main.tf
terraform {
# Write-only arguments need Terraform 1.11 or later.
required_version = ">= 1.11"
required_providers {
montecarlo = { source = "monte-carlo-data/montecarlo" }
}
}
# Endpoint from the profile's mcd_api_endpoint when set; otherwise pass it in (no default URL).
variable "montecarlo_endpoint" {
type = string
default = null # null means "use the profile's endpoint"; never a URL here
}
provider "montecarlo" {
endpoint = var.montecarlo_endpoint
}
variable "snowflake_account" { type = string } # CURRENT_ACCOUNT() + region
variable "snowflake_user" { type = string } # the user the public key is set on
variable "snowflake_warehouse" { type = string } # API-optional; queries fail without one
variable "snowflake_private_key_path" { type = string } # read locally by Terraform only
variable "warehouse_name" { type = string }
variable "connection_name" { type = string }
locals {
# Your enabled cloud deployment, found by Claude; reused, not managed here.
cloud_deployment_id = "<cloud-deployment-id>"
}
resource "montecarlo_snowflake_credentials" "snowflake" {
account = var.snowflake_account
user = var.snowflake_user
warehouse = var.snowflake_warehouse
# Write-only: never stored in state or a plan. Changing the key alone plans nothing; bump the
# version with it.
private_key_wo = file(pathexpand(var.snowflake_private_key_path)) # PEM, BEGIN/END included
private_key_wo_version = 1
}
resource "montecarlo_warehouse" "snowflake" {
name = var.warehouse_name
type = "snowflake"
deployment_id = local.cloud_deployment_id
}
resource "montecarlo_connection" "snowflake" {
name = var.connection_name
warehouse_id = montecarlo_warehouse.snowflake.id
credentials_id = montecarlo_snowflake_credentials.snowflake.id
# job_types omitted: Snowflake's defaults
}
output "created_ids" {
value = {
credentials = montecarlo_snowflake_credentials.snowflake.id
warehouse = montecarlo_warehouse.snowflake.id
connection = montecarlo_connection.snowflake.id
}
}terraform.tfvars
snowflake_account = "<account-identifier>"
snowflake_user = "<snowflake-user>"
snowflake_warehouse = "<snowflake-warehouse>"
snowflake_private_key_path = "<path-to-key.p8>"
warehouse_name = "<warehouse-name>"
connection_name = "<connection-name>"For an encrypted key, Claude adds the passphrase as another write-only argument, read from an environment variable or an uncommitted .tfvars file.
4. Apply
terraform init
terraform plan
terraform applyThe plan shows the key only as private_key_wo = (write-only attribute): it isn't in the plan, the saved plan file or the state. Send Claude the created_ids output; these ids aren't secrets.
5. Claude validates the connection
Claude runs the connection check. In this example all 13 checks passed: the connection with the stored key pair, the storage bucket, query and copy history and tag references from SNOWFLAKE.ACCOUNT_USAGE (which confirms the IMPORTED PRIVILEGES grant), and database, table and column metadata with test queries. The run ends with:
Created in this run (with your terraform apply)
credentials <id> snowflake mc_managed
warehouse <id> <warehouse-name> snowflake
connection <id> <connection-name> snowflake
validation <id> passed 13/13 passed
Reused (not part of any cleanup)
deployment <id> <cloud-deployment-name> CLOUD enabled=true
Cleanup if you abandon this: terraform destroy (connection, warehouse, credentials).
The cloud deployment isn't in this configuration, so destroy leaves it alone.
Collection starts on its own; the first metadata appears within about an hour.
Rotating the key
Put the new private key at the same path (or change snowflake_private_key_path), set the new public key on the Snowflake user, and raise private_key_wo_version to 2 in the same change. Terraform only sends a write-only value when its version changes.
Troubleshooting
| Symptom | Cause and fix |
|---|---|
terraform apply fails creating the credentials with User is not authorized … explicit deny | The provider is calling another Monte Carlo environment than your API key's. Check montecarlo whoami; if your profile has no endpoint, set montecarlo_endpoint to your environment's API URL. |
| An error about write-only arguments | Terraform is older than 1.11. |
Validation fails with JWT token is invalid | snowflake_user isn't the user the public key is set on, or the file isn't the matching private key. |
| Validation fails with "No active warehouse selected" | snowflake_warehouse is missing or the user can't use it. |
| Query history checks fail | The user is missing IMPORTED PRIVILEGES on the SNOWFLAKE database. |
| Snowflake refuses the connection | A Snowflake network policy doesn't allow Monte Carlo's IP addresses. |
Updated about 1 hour ago
