0%
(updated )5 min read🟢BeginnerTerraform

Terraform for Data Platforms: Modules for Warehouses, Not Click-Ops Grants

Click-ops Snowflake grants will not exist in the next account. Modules for warehouse plus IAM, secrets out of state, UI to explore, Terraform for anything that must last.

D
Dinesh ChandraSenior Data & AI Engineer · SnowPro Certified
Illustrated overview of Terraform for Data Platforms: Modules for Warehouses, Not Click-Ops Grants
  • ✓If it must exist next quarter, it is a module, not a console screenshot.
  • ✓Warehouse, database, and IAM grants ship together so a role cannot outlive its warehouse.
  • ✓Secrets stay in a manager. State still leaks if you put the secret in a resource argument.
  • ✓UI is for exploration. Apply is for prod. Drift is a PR, not a Friday click.
  • ✓Data engineers own schemas and job IAM. Platform owns the account and the backend.
Table of contents

A vendor POC needed a Snowflake account “by Friday.” I clicked warehouses, roles, and grants for three days. ACCOUNTADMIN stayed on a shared user. Six months later that POC was production. An audit asked us to recreate the grant graph. Nobody could. Forty warehouses were idle. The next account we built from Terraform in two hours. The first one we still cannot explain.

Click-ops is fine for a tour. It is how mystery roles and orphan compute show up in every warehouse I inherit. The unit of work is a plan someone can review, not a screenshot of the admin UI.

One module: warehouse plus IAM

I do not apply a warehouse in one PR and “add grants later.” Later is how TRANSFORMER gets SELECT on ANALYTICS from a human in the UI and never appears in git. The module takes a warehouse size, auto-suspend, a list of roles, and the databases they may use.

flowchart TD
  pr["Module PR"] --> plan["terraform plan"]
  plan --> apply["apply prod"]
  apply --> wh["Warehouse"]
  apply --> role["Role + grants"]
  apply --> iam["Cloud IAM / SP"]
  ui["Console click"] --> drift["Drift vs state"]
  drift --> pr

Explore in the UI. Promote through a module. Drift comes back as a plan, not tribal knowledge.

# modules/snowflake_wh/main.tf — shape I actually use
resource "snowflake_warehouse" "this" {
  name           = var.name
  warehouse_size = var.size
  auto_suspend   = 60
  auto_resume    = true
  comment        = var.comment
}

resource "snowflake_account_role" "usage" {
  name    = "${var.name}_USAGE"
  comment = "Runtime role for ${var.name}"
}

resource "snowflake_grant_privileges_to_account_role" "wh" {
  account_role_name = snowflake_account_role.usage.name
  privileges        = ["USAGE"]
  on_account_object {
    object_type = "WAREHOUSE"
    object_name = snowflake_warehouse.this.name
  }
}

Call it twice: transform_m and bi_s. dbt gets the first. Analysts get the second. Sharing one Huge “for convenience” is how a dashboard and a backfill fight, the same neighbor problem I see on Fabric CUs and on AWS warehouses-as-a-service. The module makes the second warehouse cheaper than another Slack thread.

State lives in a locked backend (S3 + Dynamo, Azure blob + lease, GCS + prefix). Who may apply prod is a CI role, not every data engineer’s laptop. I review plans that touch grants the way I review a firewall change.

Secrets are not in .tf and barely in state

A password = var.okta_client_secret still lands in state. I use the secret manager data source or a vault provider and I still treat state as sensitive: encryption, tight IAM, no state in Slack. Service users get key-pair or workload identity, not a password someone pasted into Terraform Cloud in 2024.

# Do not do this.
# variable "snowflake_password" { sensitive = true }
# provider "snowflake" { password = var.snowflake_password }

# Do this shape: key pair from a manager, or OIDC to the provider.
data "aws_secretsmanager_secret_version" "keypair" {
  secret_id = "prod/snowflake/tf-deployer"
}

Rotate by replacing the secret version and re-applying. Do not leave the old password in an old state file on a laptop.

UI to explore, Terraform to keep

I still click a new Snowflake feature on a sandbox. If we keep it, it becomes a resource or it gets destroyed. The rule I write in the README: if it must exist next quarter, it is in the module. Temporary warehouses get a ttl tag and a janitor. ACCOUNTADMIN is break-glass, not a daily role.

OpenTofu versus Terraform is not a grant PR. Pick what the org licensed. Data engineers write modules for schemas, warehouses, and job IAM. Platform owns the account, network, and backend. A hard split with no PR path is how click-ops returns through the side door.

# Import the click-ops leftover, then plan until empty.
# Recreating a production database to “get it in code” is not
# a migration.
#   terraform import snowflake_warehouse.transform_m TRANSFORM_M

resource "snowflake_tag" "ttl" {
  name     = "ttl"
  database = snowflake_database.ops.name
  schema   = "PUBLIC"
}

resource "snowflake_tag_association" "sandbox_wh" {
  object_type = "WAREHOUSE"
  object_name = snowflake_warehouse.sandbox.name
  tag_id      = snowflake_tag.ttl.id
  tag_value   = "2026-09-30"
}

I run a weekly janitor that lists warehouses without a module address and warehouses past ttl. Forty idle warehouses was not a Snowflake problem. It was a missing destroy path.

What data engineers may change

I give the data team a module and a PR path for:

  • warehouses and auto-suspend
  • databases / schemas they own
  • job service roles and grants on their objects

Platform keeps:

  • account / org
  • network and private link
  • state backend and who can apply prod
  • ACCOUNTADMIN and break-glass

No PR path is how click-ops returns. A 4,000-line root module is how nobody reviews grants.

Hub: Terraform. Cost neighbor: Snowflake warehouse sizing.

FAQ

OpenTofu or Terraform? Whatever the org will support. The grant graph does not care. Do not run both against the same state.

How do I import six months of click-ops? terraform import the objects you will keep, plan until empty, then delete the rest in the UI from the plan. Recreating a prod database to “start clean” is a data-loss story.

Can I put the private key in CI variables? Prefer OIDC / workload identity. If you must use a key, it lives in the secret manager, not in the Terraform Cloud variable list as a souvenir.

Why did apply change a grant I did not touch? Drift from a Friday click, or a provider rename. The plan is the truth. Apply it or revert the UI the same day.

Who runs terraform apply? CI, on main, after review. Laptop apply to prod is how two plans race.

Pitfalls

A 4,000-line main.tf. Nobody reviews it. Split by warehouse or by domain. One module, one apply blast radius.

Secrets in variables “because they are sensitive.” Sensitive hides them from the terminal. State still has them. Use a manager and key-pair or OIDC.

Laptop apply to prod. The plan that ran is not the plan that was reviewed. CI apply, locked backend, two people on grant changes.

UI grant on Friday, ticket on Monday. Monday never comes. If the grant must exist next quarter, it is a PR the same day or it is revoked.

Data team blocked from the repo. They will click. Give them a module and a PR path for schemas and job roles.

The POC account taught me the cost of “just this once.” The rebuild taught me the cost of a 4,000-line main.tf. Small modules, one warehouse plus its grants, secrets out of the arguments, apply through CI. That is the whole practice. The console is a flashlight. It is not the source of truth.

Share this post:X / TwitterLinkedInRedditWhatsApp
D

Dinesh Chandra

Senior Data & AI Engineer · SnowPro Core & Gen AI Certified

4+ years building production data platforms on Snowflake, Databricks, AWS, and Azure. I write from hands-on experience with Medallion architectures, dbt, Airflow, PySpark, and Snowflake Cortex AI. Every tutorial on this blog reflects real pipeline decisions — not documentation rewrites.

Read more about the author →

Search DataLane

↑↓ navigate↵ openesc close