Example: Create the Snowflake service user, agent and connection with Terraform

Claude writes one Terraform configuration that creates Monte Carlo's Snowflake service user, role, warehouse and grants, a new AWS collection agent, and the Snowflake connection. You run one apply; Claude then validates the connection.

In this example Claude writes one Terraform configuration that sets up both sides:

  • In Snowflake: the service user MONTE_CARLO, role MONTE_CARLO_ROLE, warehouse MONTE_CARLO_WH and the grants from Snowflake. It replaces running the setup script by hand.
  • In AWS and Monte Carlo: the deployment, the agent (with Monte Carlo's mcd-agent module) and its registration, a Secrets Manager secret holding the new user's key, the credentials reference, the warehouse and the connection.

You generate the service user's key pair yourself. Terraform reads both key files by path, and the private key reaches the secret through a write-only argument, so it never lands in Terraform state, in a plan or in the chat.

WhoWhatHow
YouGenerate the service user's key pairopenssl
ClaudeRead your account, check for duplicates, write the TerraformMCP tools, then files in your working directory
YouApply: Snowflake user, role, warehouse and grants; deployment, agent, registration; the secret and the agent's read grant; credentials reference, warehouse, connectionterraform apply
ClaudeValidate the connection: it also checks that the agent reads the secret and signs in to Snowflake as the new uservalidate_connection, get_validation_run

If the service user already exists, see Onboard Snowflake through a new AWS agent, all with Terraform instead. That example comes from an earlier run and splits the work into two applies with a credential check between them; this one creates everything in a single apply and validates afterwards.

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, with the Monte Carlo provider (monte-carlo-data/montecarlo) authenticated through your Monte Carlo CLI profile (~/.mcd/profiles.ini) or MCD_DEFAULT_API_ID / MCD_DEFAULT_API_TOKEN. Run montecarlo whoami to confirm which account and environment the profile reaches.
  • AWS credentials for the account the agent runs in, able to create IAM roles, Lambda functions, S3 buckets and Secrets Manager secrets.
  • A Snowflake admin login for Terraform, using a key pair, with ACCOUNTADMIN: the role this example was tested with. A custom role with CREATE USER, CREATE ROLE, CREATE WAREHOUSE and MANAGE GRANTS on the account should also work, including the IMPORTED PRIVILEGES grant on the SNOWFLAKE database that query logs need, but this example did not test it.
  • Your organization and account name, from SELECT CURRENT_ORGANIZATION_NAME(), CURRENT_ACCOUNT_NAME();. The Snowflake provider signs in with these, not with an account locator.

1. Generate the service user's key pair

On the machine that runs Terraform. The key must be unencrypted PKCS#8, because the agent reads it without a passphrase:

(
  umask 077
  mkdir -p ~/.secrets
  cd ~/.secrets
  openssl genrsa 2048 | openssl pkcs8 -topk8 -nocrypt -out mc_snowflake_rsa_key.p8
  openssl rsa -in mc_snowflake_rsa_key.p8 -pubout -out mc_snowflake_rsa_key.pub
)

The parentheses run it in a subshell: the files are created readable only by you, and your shell stays in its current folder.

You hand Claude the two file paths, never the files. In the examples below, <home> is your home folder as an absolute path (/Users/<you> on macOS, /home/<you> on Linux): Terraform's file() does not expand ~.

2. Ask Claude

/monte-carlo-onboarding connect our Snowflake through a new AWS agent in <region>, all with Terraform in one configuration, including creating the Monte Carlo service user, role, warehouse and grants in Snowflake. Terraform signs in to Snowflake as <ADMIN_USER> (role ACCOUNTADMIN) with the key at <home>/.secrets/<admin-key>.p8. Monitor the <DATABASE> database.

Claude reads your account first. If the same Snowflake account is already connected, it says so, because a second connection would collect the same data twice. It also points out an existing agent you could reuse. It then proposes the plan, using the docs' names for the Snowflake objects, along with one checklist of the inputs it still needs.

3. Answer Claude's questions

1. Organization <ORG_NAME>, account <ACCOUNT_NAME>.
2. The admin key is not encrypted.
3. Service user key pair: private <home>/.secrets/mc_snowflake_rsa_key.p8, public <home>/.secrets/mc_snowflake_rsa_key.pub.
4. Schema-level future grants: none.
5. No row access policies.
6. Public internet, no network policy.
7. Your names are fine; keep the docs' Snowflake names (MONTE_CARLO, MONTE_CARLO_ROLE, MONTE_CARLO_WH).
8. I run Terraform with an admin identity in AWS account <account-id>; credentials come from the environment. montecarlo whoami shows <your Monte Carlo account>.
  • Schema-level future grants: for item 4, run SHOW FUTURE GRANTS IN SCHEMA <DATABASE>.<schema>; for each schema and send only which schemas and object types it lists. Those grants override the database-level ones, so Claude adds a matching grant for each.
  • Row access policies: if you use them, MONTE_CARLO_ROLE must be exempt, or monitors fail.
  • Existing objects: if a user, role or warehouse with the proposed name already exists, Claude asks whether to pick another name or terraform import it.
  • Private networking: if Snowflake is only reachable privately, give the private subnets. The agent then runs in them, and the module takes them as private_subnets.

4. The Terraform Claude writes

Claude writes the files into your working directory. No input has a default except montecarlo_endpoint, which falls back to your Monte Carlo profile's endpoint, so a missing value stops terraform plan and names it. Nothing in these files, the plan or the state is secret. The provider's Snowflake login is read from a file and is not kept in state either.

versions.tf

terraform {
  # Write-only arguments need Terraform 1.11 or later.
  required_version = ">= 1.11"

  required_providers {
    # 0.1.27 is the first with `montecarlo_hosting_private_link`.
    montecarlo = { source = "monte-carlo-data/montecarlo", version = ">= 0.1.27" }
    # 6.50 is the first with `secret_string_wo`.
    aws       = { source = "hashicorp/aws", version = ">= 6.50" }
    snowflake = { source = "snowflakedb/snowflake", version = ">= 2.22" }
  }

  # Recommended: a remote backend that encrypts state and limits who can read it, e.g.
  # backend "s3" { bucket = "…", key = "monte-carlo/onboarding.tfstate", region = "us-east-1", encrypt = true }
}

# Endpoint from the profile's mcd_api_endpoint when set; otherwise pass it in (no default).
provider "montecarlo" {
  endpoint = var.montecarlo_endpoint
}

# AWS credentials come from the environment (AWS_ACCESS_KEY_ID / AWS_PROFILE / …).
provider "aws" {
  region              = var.aws_region
  allowed_account_ids = [var.aws_account_id] # refuse to apply with credentials for another account
}

provider "snowflake" {
  organization_name = var.snowflake_organization_name
  account_name      = var.snowflake_account_name
  user              = var.snowflake_admin_user
  role              = var.snowflake_admin_role
  authenticator     = "SNOWFLAKE_JWT"
  private_key       = file(var.snowflake_admin_private_key_path) # provider config is not kept in state
  # Encrypted admin key: export SNOWFLAKE_PRIVATE_KEY_PASSPHRASE in the shell instead.
}

variables.tf

# Every input is set in terraform.tfvars (uncommitted). No credential field has a default.

variable "montecarlo_endpoint" {
  type    = string
  default = null # null means "use the profile's endpoint"; never a URL here
}

# AWS agent
variable "aws_region" { type = string }
variable "aws_account_id" { type = string } # the AWS account the agent goes into
variable "deployment_name" { type = string }

# Snowflake admin login Terraform signs in with
variable "snowflake_organization_name" { type = string } # SELECT CURRENT_ORGANIZATION_NAME();
variable "snowflake_account_name" { type = string }      # SELECT CURRENT_ACCOUNT_NAME();
variable "snowflake_admin_user" { type = string }
variable "snowflake_admin_role" { type = string }
variable "snowflake_admin_private_key_path" { type = string } # absolute path; file() does not expand ~

variable "snowflake_databases" { type = set(string) } # the databases Monte Carlo monitors

# The Snowflake objects to create; the docs' names are MONTE_CARLO, MONTE_CARLO_ROLE, MONTE_CARLO_WH.
variable "snowflake_user_name" { type = string }
variable "snowflake_role_name" { type = string }
variable "snowflake_warehouse_name" { type = string }

# The Monte Carlo service user's key pair, generated by you; absolute paths.
variable "snowflake_public_key_path" { type = string }
variable "snowflake_private_key_path" { type = string }

# Monte Carlo warehouse and connection
variable "warehouse_name" { type = string }
variable "connection_name" { type = string }

terraform.tfvars (non-secret values; keep it out of version control)

# Paths and names only; no secret values. Keep this file out of version control.

aws_region      = "<region>"
aws_account_id  = "<account-id>"
deployment_name = "<deployment-name>"

snowflake_organization_name      = "<ORG_NAME>"
snowflake_account_name           = "<ACCOUNT_NAME>"
snowflake_admin_user             = "<ADMIN_USER>"
snowflake_admin_role             = "ACCOUNTADMIN"
snowflake_admin_private_key_path = "<home>/.secrets/<admin-key>.p8"

snowflake_databases = ["<DATABASE>"]

snowflake_user_name      = "MONTE_CARLO"
snowflake_role_name      = "MONTE_CARLO_ROLE"
snowflake_warehouse_name = "MONTE_CARLO_WH"

snowflake_public_key_path  = "<home>/.secrets/mc_snowflake_rsa_key.pub"
snowflake_private_key_path = "<home>/.secrets/mc_snowflake_rsa_key.p8"

warehouse_name  = "<warehouse-name>"
connection_name = "<connection-name>"

snowflake.tf

# Replaces the setup script in https://docs.getmontecarlo.com/docs/snowflake.

locals {
  # Each key file read once; the body extraction and the preconditions use this.
  snowflake_key_pem = {
    public  = file(var.snowflake_public_key_path)
    private = file(var.snowflake_private_key_path)
  }
  # PEM body without the BEGIN/END lines: what rsa_public_key and the secret's private_key take.
  snowflake_key_body = { for k, pem in local.snowflake_key_pem : k => join("", [
    for line in split("\n", pem) : trimspace(line) if !startswith(trimspace(line), "-----")
  ]) }
  # Unencrypted PKCS#8, which the self-hosted secret and the managed credential both need.
  snowflake_private_key_ok = (
    strcontains(local.snowflake_key_pem.private, "-----BEGIN PRIVATE KEY-----") &&
    !strcontains(local.snowflake_key_pem.private, "ENCRYPTED")
  )
  # Changes whenever the key pair does, so a rotation always re-sends the private key.
  snowflake_key_version = parseint(substr(sha256(local.snowflake_key_body.public), 0, 12), 16)

  # Per object type: the docs' REFERENCES / MONITOR / SELECT grants, on existing and future objects.
  snowflake_object_privileges = {
    "TABLES"          = ["REFERENCES", "SELECT"]
    "VIEWS"           = ["REFERENCES", "SELECT"]
    "EXTERNAL TABLES" = ["REFERENCES", "SELECT"]
    "DYNAMIC TABLES"  = ["MONITOR", "SELECT"]
    "STREAMS"         = ["SELECT"]
  }
  snowflake_object_grants = { for g in flatten([
    for db in var.snowflake_databases : [
      for plural, privileges in local.snowflake_object_privileges : [
        for scope in ["all", "future"] : { database = db, plural = plural, privileges = privileges, scope = scope }
      ]
    ]
  ]) : "${g.database}/${g.plural}/${g.scope}" => g }
}

resource "snowflake_account_role" "monte_carlo" { name = var.snowflake_role_name }

resource "snowflake_warehouse" "monte_carlo" {
  name                = var.snowflake_warehouse_name
  warehouse_size      = "XSMALL"
  initially_suspended = true
  auto_suspend        = 300
  auto_resume         = "true"
}

resource "snowflake_service_user" "monte_carlo" {
  name              = var.snowflake_user_name
  default_role      = snowflake_account_role.monte_carlo.name
  default_warehouse = snowflake_warehouse.monte_carlo.name
  rsa_public_key    = local.snowflake_key_body.public

  lifecycle {
    precondition {
      condition     = strcontains(local.snowflake_key_pem.public, "BEGIN PUBLIC KEY")
      error_message = "snowflake_public_key_path must be a PEM public key (openssl rsa -pubout)."
    }
  }
}

resource "snowflake_grant_account_role" "monte_carlo" {
  role_name = snowflake_account_role.monte_carlo.name
  user_name = snowflake_service_user.monte_carlo.name
}

resource "snowflake_grant_privileges_to_account_role" "warehouse" {
  account_role_name = snowflake_account_role.monte_carlo.name
  privileges        = ["OPERATE", "USAGE", "MONITOR"]
  on_account_object {
    object_type = "WAREHOUSE"
    object_name = snowflake_warehouse.monte_carlo.name
  }
}

# Query history, for query logs and lineage.
resource "snowflake_grant_privileges_to_account_role" "query_history" {
  account_role_name = snowflake_account_role.monte_carlo.name
  privileges        = ["IMPORTED PRIVILEGES"]
  on_account_object {
    object_type = "DATABASE"
    object_name = "SNOWFLAKE"
  }
}

resource "snowflake_grant_privileges_to_account_role" "database" {
  for_each          = var.snowflake_databases
  account_role_name = snowflake_account_role.monte_carlo.name
  privileges        = ["USAGE", "MONITOR"]
  on_account_object {
    object_type = "DATABASE"
    object_name = each.value
  }
}

resource "snowflake_grant_privileges_to_account_role" "all_schemas" {
  for_each          = var.snowflake_databases
  account_role_name = snowflake_account_role.monte_carlo.name
  privileges        = ["USAGE", "MONITOR"]
  on_schema { all_schemas_in_database = each.value }
}

resource "snowflake_grant_privileges_to_account_role" "future_schemas" {
  for_each          = var.snowflake_databases
  account_role_name = snowflake_account_role.monte_carlo.name
  privileges        = ["USAGE", "MONITOR"]
  on_schema { future_schemas_in_database = each.value }
}

# No schema-level future grants in <database> (checked), so no in_schema grants.
resource "snowflake_grant_privileges_to_account_role" "objects" {
  for_each          = local.snowflake_object_grants
  account_role_name = snowflake_account_role.monte_carlo.name
  privileges        = each.value.privileges
  on_schema_object {
    dynamic "all" {
      for_each = each.value.scope == "all" ? [1] : []
      content {
        object_type_plural = each.value.plural
        in_database        = each.value.database
      }
    }
    dynamic "future" {
      for_each = each.value.scope == "future" ? [1] : []
      content {
        object_type_plural = each.value.plural
        in_database        = each.value.database
      }
    }
  }
}

# The one place that lists every grant: the connection depends on this. Add any in_schema grant here.
resource "terraform_data" "snowflake_grants_ready" {
  depends_on = [
    snowflake_grant_account_role.monte_carlo,
    snowflake_grant_privileges_to_account_role.warehouse,
    snowflake_grant_privileges_to_account_role.query_history,
    snowflake_grant_privileges_to_account_role.database,
    snowflake_grant_privileges_to_account_role.all_schemas,
    snowflake_grant_privileges_to_account_role.future_schemas,
    snowflake_grant_privileges_to_account_role.objects,
  ]
}

agent.tf

# Monte Carlo's AWS account, which assumes the agent's invoker role.
data "montecarlo_hosting_private_link" "this" {}

resource "montecarlo_deployment" "agent" {
  name             = var.deployment_name
  type             = "COLLECTION_AGENT"
  runtime_platform = "AWS"
}

# https://registry.terraform.io/modules/monte-carlo-data/mcd-agent/aws
# No private_subnets: Snowflake is reached over the public internet.
module "mcd_agent" {
  source  = "monte-carlo-data/mcd-agent/aws"
  version = "1.0.7"

  region           = var.aws_region
  cloud_account_id = data.montecarlo_hosting_private_link.this.aws.account_id
  external_id      = montecarlo_deployment.agent.aws_external_id
}

resource "montecarlo_aws_collection_agent" "agent" {
  deployment_id       = montecarlo_deployment.agent.id
  lambda_function_arn = module.mcd_agent.mcd_agent_function_arn
  role_arn            = module.mcd_agent.mcd_agent_invoker_role_arn
}

# The self-hosted secret the agent reads at query time.
resource "aws_secretsmanager_secret" "snowflake" {
  name = "monte-carlo/${var.deployment_name}/snowflake"
}

resource "aws_secretsmanager_secret_version" "snowflake" {
  secret_id = aws_secretsmanager_secret.snowflake.id
  # Write-only: the private key never reaches state or a plan.
  secret_string_wo = jsonencode({
    connect_args = {
      # Account names may contain underscores; the connector's hostname form uses hyphens.
      account     = "${var.snowflake_organization_name}-${replace(var.snowflake_account_name, "_", "-")}"
      user        = snowflake_service_user.monte_carlo.name
      warehouse   = snowflake_warehouse.monte_carlo.name
      private_key = local.snowflake_key_body.private
    }
  })
  secret_string_wo_version = local.snowflake_key_version

  lifecycle {
    precondition {
      condition     = local.snowflake_private_key_ok
      error_message = "snowflake_private_key_path must be an unencrypted PKCS#8 key (openssl pkcs8 -topk8 -nocrypt)."
    }
  }
}

resource "aws_iam_role_policy" "agent_read_secret" {
  role = module.mcd_agent.mcd_agent_execution_role.name
  policy = jsonencode({
    Version = "2012-10-17"
    Statement = [{
      Effect   = "Allow"
      Action   = "secretsmanager:GetSecretValue"
      Resource = aws_secretsmanager_secret.snowflake.arn
    }]
  })
}

montecarlo.tf

resource "montecarlo_self_hosted_aws_credentials" "snowflake" {
  connection_type = "snowflake"
  aws_secret      = aws_secretsmanager_secret.snowflake.arn
  depends_on      = [aws_iam_role_policy.agent_read_secret, aws_secretsmanager_secret_version.snowflake]
}

resource "montecarlo_warehouse" "snowflake" {
  name          = var.warehouse_name
  type          = "snowflake"
  deployment_id = montecarlo_deployment.agent.id
  depends_on    = [montecarlo_aws_collection_agent.agent]
}

resource "montecarlo_connection" "snowflake" {
  name           = var.connection_name
  warehouse_id   = montecarlo_warehouse.snowflake.id
  credentials_id = montecarlo_self_hosted_aws_credentials.snowflake.id
  # job_types omitted: the type's defaults.
  # Collection never starts before the grants exist.
  depends_on = [terraform_data.snowflake_grants_ready]
}

outputs.tf

output "created_ids" { # ids only
  value = {
    deployment  = montecarlo_deployment.agent.id
    agent       = montecarlo_aws_collection_agent.agent.id
    credentials = montecarlo_self_hosted_aws_credentials.snowflake.id
    warehouse   = montecarlo_warehouse.snowflake.id
    connection  = montecarlo_connection.snowflake.id
  }
}

5. Apply

terraform init
terraform apply
terraform output -json created_ids

One apply creates the Snowflake user, role, warehouse and grants, the deployment, the agent's Lambda function, roles and bucket, its registration, the secret and the agent's read grant, then the credentials reference, the warehouse and the connection. The connection waits for every grant, so collection never starts before them. Send Claude the created_ids output.

If the apply stops partway, what it already created stays in the Terraform state. Fix the cause and run terraform apply again; it continues from there instead of starting over.

Claude then validates the connection. The checks include the agent reading the secret and signing in to Snowflake as MONTE_CARLO; in this example all 15 passed: the secret, the Snowflake login, storage, query history, metadata and a sample SELECT. If one fails, Claude relays the reason and the fix: fix it, run terraform apply if you changed the configuration, then ask Claude to validate again. An apply alone doesn't re-run the checks. It ends with a summary:

Created in this run (by your Terraform)
  deployment    <id>  <deployment-name>  COLLECTION_AGENT/AWS  enabled=true
  agent         <id>
  snowflake     MONTE_CARLO / MONTE_CARLO_ROLE / MONTE_CARLO_WH + grants on <DATABASE>
  credentials   <id>  snowflake  aws_secrets_manager
  warehouse     <id>  <warehouse-name>  snowflake
  connection    <id>  <connection-name>  snowflake
  validation    <id>  passed  15/15 passed

Reused (excluded from cleanup)
  none

Cleanup if you abandon this: terraform destroy in your working directory (not the Monte Carlo
delete tools).

Collection starts on its own; the first metadata appears within about an hour.

Rotate the service user's key

Generate a new key pair, point the two path variables at it, and run terraform apply. The secret's version is derived from the public key, so the same apply sets the new public key on the service user and writes the new private key to the secret. The two updates are separate steps, though: between them the agent's logins fail, and if the apply stops in between, they stay mismatched until you apply again.

For a rotation without that gap, keep a valid key in one of Snowflake's two key slots at every step. Each step is its own apply, so it doesn't depend on how the provider orders the changes within one update:

  1. Add rsa_public_key_2 to snowflake_service_user.monte_carlo with the new public key's body (stripped of its BEGIN/END lines, as local.snowflake_key_body does), and apply. The user accepts both keys; the secret still holds the old private key, which keeps working.
  2. Point the path variables at the new pair, set rsa_public_key_2 to the old public key's body, and apply. The new key moves to the first slot and the secret gets the new private key; the old key stays valid in the second slot meanwhile.
  3. Once Claude's connection validation passes with the new key, remove rsa_public_key_2 and apply again.

We tested this on the same setup: during step 2, logins with both keys kept succeeding, and Monte Carlo's validation passed with the new key; after step 3 the old key is rejected.

Clean up

terraform destroy

It removes the connection, warehouse and credentials, the agent registration and deployment, and the module's AWS resources, schedules the secret for deletion (Secrets Manager's 30-day recovery window; see the troubleshooting row on already scheduled for deletion), then removes the grants, the user, the role and the warehouse in Snowflake. Empty the agent's versioned S3 bucket first if it holds objects, old versions included. Wait until the first collection has finished before you destroy: see the troubleshooting row on Delete deployment failed.

Troubleshooting

SymptomCause and fix
Plan fails with JWT token is invalid from the Snowflake providerThe admin user doesn't have the public key of the file Terraform reads. Compare DESC USER <ADMIN_USER> → RSA_PUBLIC_KEY_FP with the key's fingerprint, or reset it with ALTER USER … SET RSA_PUBLIC_KEY = '…'.
The apply fails creating the user, role or a grant (Insufficient privileges)The admin role can't create users, roles or warehouses, or can't grant. Use ACCOUNTADMIN, the role this example was tested with. A custom role with CREATE USER, CREATE ROLE, CREATE WAREHOUSE and MANAGE GRANTS on the account should also work, but is untested here.
Plan stops with snowflake_private_key_path must be an unencrypted PKCS#8 keyThe key is encrypted or in PKCS#1 format. Convert it into a new file and point snowflake_private_key_path at it; the public key stays the same. Encrypted PKCS#8 (BEGIN ENCRYPTED PRIVATE KEY): openssl pkcs8 -in <key> -out mc_snowflake_rsa_key_plain.p8. PKCS#1 (BEGIN RSA PRIVATE KEY): openssl pkcs8 -topk8 -nocrypt -in <key> -out mc_snowflake_rsa_key_plain.p8. Never use the same file name for -in and -out.
Plan fails parsing the admin keyThe admin key is encrypted. Export SNOWFLAKE_PRIVATE_KEY_PASSPHRASE in your shell before terraform plan; never put it in terraform.tfvars.
The apply fails with already exists for the user, role or warehousePick another name in terraform.tfvars, or bring the object under Terraform with terraform import.
The provider asks to enable an experimental feature for accountIt signs in with organization_name and account_name; an account locator such as xy12345.us-east-1 needs the provider's experimental account fallback. Use the names from CURRENT_ORGANIZATION_NAME() and CURRENT_ACCOUNT_NAME().
The connection validation fails with JWT token is invalidThe two path variables point at files from different key pairs. Check that openssl pkey -in <private_key> -pubout prints the same key as the public file, fix the path, and apply.
The apply fails creating the secret: a secret with this name is already scheduled for deletionAn earlier terraform destroy left the secret in Secrets Manager's recovery window (30 days by default). Restore it and bring it back under Terraform (aws secretsmanager restore-secret --secret-id <name>, then terraform import aws_secretsmanager_secret.snowflake <arn>), or use another deployment_name. For a throwaway test setup, recovery_window_in_days = 0 on aws_secretsmanager_secret.snowflake deletes it immediately on destroy.
terraform destroy fails with Delete deployment failed: Internal server errorThe delete ran while Monte Carlo was still loading the warehouse's first metadata, and was rolled back. A second destroy can then report success while the deployment is only deactivated. Wait for the first collection to finish before destroying; if you hit this error, contact Monte Carlo support with the request ID rather than assuming the deployment is gone.
The connection validation fails with "Could not connect"The execution role can't read the secret: check aws_iam_role_policy.agent_read_secret, and add kms:Decrypt for a customer-managed key.
The apply fails creating montecarlo_deployment with User is not authorized … explicit denyThe provider is calling a different Monte Carlo environment than the one your API key belongs to. Set montecarlo_endpoint to your profile's endpoint (montecarlo whoami confirms the account) and apply again.
Monte Carlo also collects a database you didn't grant, such as SNOWFLAKE_LEARNING_DB on a trial accountIt is shared with the PUBLIC role, which every user has. Revoke it from PUBLIC in Snowflake, or exclude it in Monte Carlo's collection settings.

Did this page help you?