Infrastructure as Code for Data Engineering
Terraform fundamentals, provisioning Snowflake warehouses, S3 buckets, Airflow environments, IAM roles, and managing data infrastructure with state, modules, and CI/CD.
Infrastructure as Code — Why It Matters for Data Platforms
A data platform is built on infrastructure: S3 buckets, Snowflake warehouses, IAM roles, Airflow environments, VPCs, security groups, and dozens of other cloud resources. When this infrastructure is created manually through cloud consoles, it becomes invisible — nobody knows exactly what exists, who created it, or why. The dev environment drifts from production. A new environment takes a week to set up. A misconfigured IAM role exposes PII to the wrong team.
Infrastructure as Code treats cloud resources like software: defined in version-controlled files, reviewed through pull requests, tested in CI, and deployed through an automated pipeline. This module builds the Terraform configuration for FreshCart’s actual data platform — the S3 lake, the IAM roles, the Snowflake account, and the CI/CD pipeline that applies changes safely — one piece at a time.
Terraform Fundamentals — The Core Concepts Every Data Engineer Needs
Terraform is the dominant IaC tool in 2026. It has providers for every major cloud (AWS, Azure, GCP) and for data tools like Snowflake, Databricks, and Confluent. Understanding providers, resources, state, plan, and apply is sufficient to manage most data platform infrastructure.
Providers and the core workflow
terraform {
required_providers {
aws = { source = "hashicorp/aws", version = "~> 5.0" }
snowflake = { source = "Snowflake-Labs/snowflake", version = "~> 0.87" }
}
# Remote state backend (required for team use):
backend "s3" {
bucket = "freshcart-terraform-state"
key = "data-platform/terraform.tfstate"
region = "ap-south-1"
encrypt = true
dynamodb_table = "freshcart-terraform-locks" # prevents concurrent applies
}
}
provider "aws" { region = var.aws_region }
provider "snowflake" {
account = var.snowflake_account
username = var.snowflake_user
password = var.snowflake_password
role = "SYSADMIN"
}$ terraform init
Initializing the backend...
Initializing provider plugins...
- Finding hashicorp/aws versions matching "~> 5.0"...
- Finding Snowflake-Labs/snowflake versions matching "~> 0.87"...
Terraform has been successfully initialized!Reading a plan before every apply
$ terraform plan
Terraform will perform the following actions:
# aws_s3_bucket.data_lake will be created (+)
+ resource "aws_s3_bucket" "data_lake" {
+ bucket = "freshcart-data-lake-prod"
+ id = (known after apply)
}
# aws_s3_bucket.staging will be destroyed (-)
- resource "aws_s3_bucket" "staging" {
- bucket = "freshcart-staging-old"
}
# snowflake_warehouse.analytics will be updated in-place (~)
~ resource "snowflake_warehouse" "analytics" {
~ warehouse_size = "SMALL" → "MEDIUM"
}
Plan: 1 to add, 1 to change, 1 to destroy.Variables, locals, and outputs — making it reusable
variable "environment" {
description = "Deployment environment: dev, staging, or prod"
type = string
validation {
condition = contains(["dev", "staging", "prod"], var.environment)
error_message = "Environment must be dev, staging, or prod."
}
}
variable "snowflake_account" {
type = string
sensitive = true # marked sensitive: not shown in plan output
}
locals {
name_prefix = "freshcart-${var.environment}"
common_tags = { Environment = var.environment, Project = "freshcart-data-platform", ManagedBy = "terraform" }
snowflake_warehouse_size = { dev = "X-SMALL", staging = "SMALL", prod = "MEDIUM" }
}
output "data_lake_bucket_arn" {
value = aws_s3_bucket.data_lake.arn
}# terraform/environments/prod.tfvars
environment = "prod"
data_retention_days = 365
# terraform/environments/dev.tfvars
environment = "dev"
data_retention_days = 30
# Deploy to prod: terraform apply -var-file=environments/prod.tfvars
# Deploy to dev: terraform apply -var-file=environments/dev.tfvarsProvisioning an S3 Data Lake — Complete Terraform Configuration
The S3 data lake is the foundation of FreshCart’s Medallion Architecture. Its Terraform configuration covers the bucket, encryption, versioning, lifecycle policies, access logging, and the notifications that trigger downstream processing — all in version-controlled code.
The bucket, encryption, and public access blocking
resource "aws_s3_bucket" "data_lake" {
bucket = "${local.name_prefix}-data-lake"
force_destroy = var.environment == "dev" # only allow destroy in dev
tags = local.common_tags
}
resource "aws_kms_key" "data_lake" {
description = "FreshCart data lake encryption key"
enable_key_rotation = true
tags = local.common_tags
}
resource "aws_s3_bucket_server_side_encryption_configuration" "data_lake" {
bucket = aws_s3_bucket.data_lake.id
rule {
apply_server_side_encryption_by_default {
sse_algorithm = "aws:kms"
kms_master_key_id = aws_kms_key.data_lake.arn
}
bucket_key_enabled = true # reduces KMS API calls and cost
}
}
# Block all public access — critical for data lakes
resource "aws_s3_bucket_public_access_block" "data_lake" {
bucket = aws_s3_bucket.data_lake.id
block_public_acls = true
block_public_policy = true
ignore_public_acls = true
restrict_public_buckets = true
}Versioning and zone-based lifecycle rules
resource "aws_s3_bucket_versioning" "data_lake" {
bucket = aws_s3_bucket.data_lake.id
versioning_configuration { status = "Enabled" }
}
resource "aws_s3_bucket_lifecycle_configuration" "data_lake" {
bucket = aws_s3_bucket.data_lake.id
depends_on = [aws_s3_bucket_versioning.data_lake]
rule { # landing: short-lived raw files
id = "landing-zone-expiry"
status = "Enabled"
filter { prefix = "landing/" }
expiration { days = 30 }
}
rule { # bronze: transition to cheaper storage over time
id = "bronze-tiering"
status = "Enabled"
filter { prefix = "bronze/" }
transition { days = 90 storage_class = "STANDARD_IA" }
transition { days = 365 storage_class = "GLACIER" }
noncurrent_version_expiration { noncurrent_days = 30 }
}
rule { # silver/gold: standard IA after 180 days
id = "silver-gold-tiering"
status = "Enabled"
filter { or { prefix = "silver/" prefix = "gold/" } }
transition { days = 180 storage_class = "STANDARD_IA" }
}
}Access logging and event notifications
resource "aws_s3_bucket" "access_logs" {
bucket = "${local.name_prefix}-data-lake-logs"
tags = local.common_tags
}
resource "aws_s3_bucket_logging" "data_lake" {
bucket = aws_s3_bucket.data_lake.id
target_bucket = aws_s3_bucket.access_logs.id
target_prefix = "s3-access-logs/"
}
resource "aws_s3_bucket_notification" "data_lake" {
bucket = aws_s3_bucket.data_lake.id
lambda_function {
lambda_function_arn = aws_lambda_function.bronze_ingestion_trigger.arn
events = ["s3:ObjectCreated:*"]
filter_prefix = "landing/"
}
}$ terraform apply
aws_kms_key.data_lake: Creating...
aws_kms_key.data_lake: Creation complete after 4s
aws_s3_bucket.data_lake: Creating...
aws_s3_bucket.data_lake: Creation complete after 2s
...
Apply complete! Resources: 8 added, 0 changed, 0 destroyed.
Outputs:
data_lake_bucket_arn = "arn:aws:s3:::freshcart-prod-data-lake"IAM for Data Platforms — Least Privilege as Code
Four IAM roles cover FreshCart’s primary access patterns: ingestion pipelines (write to landing/bronze), transformation pipelines (read bronze, write silver/gold), analyst access (read silver/gold only), and the CI service account. Defining these in Terraform makes least privilege consistent and reviewable.
The ingestion pipeline role
resource "aws_iam_role" "pipeline_ingestion" {
name = "${local.name_prefix}-pipeline-ingestion"
assume_role_policy = data.aws_iam_policy_document.lambda_assume.json
tags = local.common_tags
}
resource "aws_iam_policy" "pipeline_ingestion" {
name = "${local.name_prefix}-pipeline-ingestion-policy"
policy = jsonencode({
Version = "2012-10-17"
Statement = [
{
Sid = "WriteLanding", Effect = "Allow"
Action = ["s3:PutObject", "s3:GetObject"]
Resource = ["${aws_s3_bucket.data_lake.arn}/landing/*", "${aws_s3_bucket.data_lake.arn}/bronze/*"]
},
{
Sid = "UseKMS", Effect = "Allow"
Action = ["kms:GenerateDataKey", "kms:Decrypt"]
Resource = aws_kms_key.data_lake.arn
},
]
})
}
resource "aws_iam_role_policy_attachment" "ingestion_policy" {
role = aws_iam_role.pipeline_ingestion.name
policy_arn = aws_iam_policy.pipeline_ingestion.arn
}Transformation and analyst roles
# TRANSFORM: reads bronze, writes silver+gold, NO access to landing (raw PII)
resource "aws_iam_policy" "pipeline_transform" {
name = "${local.name_prefix}-pipeline-transform-policy"
policy = jsonencode({
Version = "2012-10-17"
Statement = [
{ Sid = "ReadBronze", Effect = "Allow", Action = ["s3:GetObject", "s3:ListBucket"],
Resource = ["${aws_s3_bucket.data_lake.arn}/bronze/*", aws_s3_bucket.data_lake.arn] },
{ Sid = "WriteTransformed", Effect = "Allow", Action = ["s3:PutObject", "s3:DeleteObject"],
Resource = ["${aws_s3_bucket.data_lake.arn}/silver/*", "${aws_s3_bucket.data_lake.arn}/gold/*"] },
]
})
}
# ANALYST: reads silver+gold only, no raw PII access at all
resource "aws_iam_policy" "analyst" {
name = "${local.name_prefix}-analyst-policy"
policy = jsonencode({
Version = "2012-10-17"
Statement = [
{ Sid = "ReadAnalyticsLayers", Effect = "Allow", Action = ["s3:GetObject"],
Resource = ["${aws_s3_bucket.data_lake.arn}/silver/*", "${aws_s3_bucket.data_lake.arn}/gold/*"] },
]
})
}Who is allowed to assume each role
data "aws_iam_policy_document" "lambda_assume" {
statement {
actions = ["sts:AssumeRole"]
principals { type = "Service" identifiers = ["lambda.amazonaws.com"] }
}
}
data "aws_iam_policy_document" "federated_assume" {
statement {
actions = ["sts:AssumeRoleWithWebIdentity"]
principals {
type = "Federated"
identifiers = ["arn:aws:iam::${data.aws_caller_identity.current.account_id}:oidc-provider/accounts.google.com"]
}
}
}# an analyst who assumes this role and tries to read landing/ gets exactly this:
$ aws s3 cp s3://freshcart-prod-data-lake/landing/orders_raw.csv .
fatal error: An error occurred (AccessDenied) when calling the GetObject operationSnowflake Infrastructure — Warehouses, Roles, and Databases in Terraform
Snowflake’s first-class Terraform provider lets the entire account configuration — warehouses, databases, schemas, roles, grants, and users — be managed as code. Adding a new analyst becomes a one-line PR instead of a console click sequence.
Warehouses, sized per environment
resource "snowflake_warehouse" "dbt_pipeline" {
name = "${upper(var.environment)}_DBT_PIPELINE_WH"
warehouse_size = lookup(local.snowflake_warehouse_size, var.environment, "SMALL")
auto_suspend = 300 # 5 min idle → suspend
auto_resume = true
max_cluster_count = 1
}
resource "snowflake_warehouse" "analyst" {
name = "${upper(var.environment)}_ANALYST_WH"
warehouse_size = "SMALL"
auto_suspend = 600
auto_resume = true
max_cluster_count = var.environment == "prod" ? 3 : 1
scaling_policy = var.environment == "prod" ? "ECONOMY" : "STANDARD"
}
resource "snowflake_warehouse" "dashboard" {
name = "${upper(var.environment)}_DASHBOARD_WH"
warehouse_size = "X-SMALL"
auto_suspend = 60
auto_resume = true
}A resource monitor — preventing runaway cost
resource "snowflake_resource_monitor" "monthly_limit" {
name = "${upper(var.environment)}_MONTHLY_MONITOR"
credit_quota = var.environment == "prod" ? 1000 : 100
notify_triggers = [75, 90] # alert at 75% and 90%
suspend_triggers = [100] # suspend warehouses at 100%
suspend_immediately_triggers = [110] # hard stop at 110%
notify_users = ["data-team-lead@freshcart.com"]
}# Slack alert from the resource monitor, mid-month:
⚠️ PROD_MONTHLY_MONITOR at 76% of 1000 credit quota (760 credits used)
# at 100% every warehouse under this monitor suspends automatically —
# no pipeline can silently burn through an unlimited compute budgetDatabases, schemas, and zone-based roles
resource "snowflake_database" "freshcart" {
name = "FRESHCART_${upper(var.environment)}"
data_retention_time_in_days = var.environment == "prod" ? 30 : 1
}
resource "snowflake_schema" "bronze" { database = snowflake_database.freshcart.name name = "BRONZE" }
resource "snowflake_schema" "silver" { database = snowflake_database.freshcart.name name = "SILVER" }
resource "snowflake_schema" "gold" { database = snowflake_database.freshcart.name name = "GOLD" }
resource "snowflake_schema" "monitoring" { database = snowflake_database.freshcart.name name = "MONITORING" }
resource "snowflake_role" "pipeline" { name = "${upper(var.environment)}_PIPELINE_ROLE" comment = "dbt and Spark service accounts" }
resource "snowflake_role" "analyst" { name = "${upper(var.environment)}_ANALYST_ROLE" comment = "Read access to silver and gold" }
resource "snowflake_role" "bi_service" { name = "${upper(var.environment)}_BI_SERVICE_ROLE" comment = "Metabase/Tableau — read gold only" }Grants — wiring roles to schemas
resource "snowflake_schema_grant" "pipeline_silver_write" {
database_name = snowflake_database.freshcart.name
schema_name = snowflake_schema.silver.name
privilege = "CREATE TABLE"
roles = [snowflake_role.pipeline.name]
}
resource "snowflake_table_grant" "analyst_silver_select" {
database_name = snowflake_database.freshcart.name
schema_name = snowflake_schema.silver.name
privilege = "SELECT"
roles = [snowflake_role.analyst.name]
on_future = true # applies to all future tables automatically
}
resource "snowflake_warehouse_grant" "analyst_warehouse" {
warehouse_name = snowflake_warehouse.analyst.name
privilege = "USAGE"
roles = [snowflake_role.analyst.name]
}Users, driven from a single variable
variable "snowflake_analysts" {
type = list(string)
default = []
}
resource "snowflake_user" "analysts" {
for_each = toset(var.snowflake_analysts)
name = replace(each.value, "@freshcart.com", "")
email = each.value
default_role = snowflake_role.analyst.name
default_warehouse = snowflake_warehouse.analyst.name
must_change_password = true
}
resource "snowflake_role_grants" "analysts" {
for_each = toset(var.snowflake_analysts)
role_name = snowflake_role.analyst.name
users = [replace(each.value, "@freshcart.com", "")]
depends_on = [snowflake_user.analysts]
}Terraform Modules — Reusable Infrastructure Components
A Terraform module is a reusable, parameterised configuration for a set of related resources. Wrapping Part 03’s S3 setup as a data_lake module means dev and prod use the exact same tested configuration with different variable values — no environment drift, no duplicated resource blocks.
Defining the module
# modules/data_lake/variables.tf
variable "environment" { type = string }
variable "retention_days_bronze" { type = number default = 365 }
variable "enable_versioning" { type = bool default = true }
variable "tags" { type = map(string) default = {} }
# modules/data_lake/main.tf — all the S3 resources from Part 03, parameterised
resource "aws_s3_bucket" "data_lake" {
bucket = "freshcart-${var.environment}-data-lake"
tags = merge(var.tags, { Environment = var.environment })
}
# ... encryption, versioning, lifecycle resources reference var.retention_days_bronze etc.
# modules/data_lake/outputs.tf
output "bucket_arn" { value = aws_s3_bucket.data_lake.arn }
output "kms_key_id" { value = aws_kms_key.data_lake.id }Calling the same module for prod and dev
# environments/prod/main.tf
module "data_lake_prod" {
source = "../../modules/data_lake"
environment = "prod"
retention_days_bronze = 730 # 2 years for prod
enable_versioning = true
}
module "snowflake_prod" {
source = "../../modules/snowflake_env"
environment = "prod"
warehouse_size_pipeline = "MEDIUM"
analyst_cluster_count = 3
analysts = ["priya@freshcart.com", "rahul@freshcart.com"]
}
# environments/dev/main.tf — SAME modules, cheaper settings
module "data_lake_dev" {
source = "../../modules/data_lake"
environment = "dev"
retention_days_bronze = 30
enable_versioning = false # cheaper: no versioning in dev
}
module "snowflake_dev" {
source = "../../modules/snowflake_env"
environment = "dev"
warehouse_size_pipeline = "X-SMALL"
analyst_cluster_count = 1
analysts = [] # dev uses personal credentials
}data_lake module’s lifecycle policy needs a new rule for a “quarantine” zone, where does that change get made — insidemodules/data_lake/main.tf, or inside each environment’s main.tf? Why does that answer matter for keeping dev and prod actually identical in structure?CI/CD for Terraform — Safe Infrastructure Changes
Infrastructure changes carry higher risk than code changes — a wrong apply can delete a production S3 bucket or an IAM role pipelines depend on. The pipeline must require a human to review the plan before any apply, and must prevent concurrent runs.
Plan on every pull request
name: Terraform
on:
pull_request: { paths: ['terraform/**'] }
push: { branches: [main], paths: ['terraform/**'] }
jobs:
terraform-plan:
if: github.event_name == 'pull_request'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: hashicorp/setup-terraform@v3
with: { terraform_version: "1.7.0" }
- uses: aws-actions/configure-aws-credentials@v4
with: { role-to-assume: ${{ secrets.AWS_TERRAFORM_ROLE_ARN }}, aws-region: ap-south-1 }
- run: terraform init
- run: terraform fmt -check -recursive terraform/
- run: terraform validate
- name: Terraform Plan
id: plan
run: terraform plan -var-file=../../environments/prod.tfvars -out=tfplan -detailed-exitcode 2>&1 | tee plan_output.txt
continue-on-error: true
- name: Post plan as PR comment
uses: actions/github-script@v7
with:
script: |
const fs = require('fs');
const plan = fs.readFileSync('plan_output.txt', 'utf8');
const truncated = plan.length > 60000 ? plan.slice(-60000) : plan;
github.rest.issues.createComment({
issue_number: context.issue.number, owner: context.repo.owner, repo: context.repo.repo,
body: `## Terraform Plan\n\n<details><summary>Show Plan</summary>\n\n\`\`\`\n${truncated}\n\`\`\`\n</details>`,
});
- if: steps.plan.outputs.exitcode == '1'
run: exit 1Apply only on merge, with a manual approval gate
terraform-apply:
if: github.ref == 'refs/heads/main' && github.event_name == 'push'
runs-on: ubuntu-latest
environment: production # requires manual approval in GitHub Environments
steps:
- uses: actions/checkout@v4
- uses: hashicorp/setup-terraform@v3
with: { terraform_version: "1.7.0" }
- uses: aws-actions/configure-aws-credentials@v4
with: { role-to-assume: ${{ secrets.AWS_TERRAFORM_ROLE_ARN }}, aws-region: ap-south-1 }
- run: terraform init
- run: terraform apply -var-file=../../environments/prod.tfvars -auto-approve -input=falsePull request #482 opened
✓ terraform-plan: Plan: 2 to add, 1 to change, 0 to destroy. (posted as PR comment)
✓ 1 reviewer approved
✓ merged to main
⏸ terraform-apply: waiting for approval (environment: production)
✓ approved by data-team-lead
✓ terraform-apply: Apply complete! Resources: 2 added, 1 changed, 0 destroyed.Protecting critical resources from accidental destroy
resource "aws_s3_bucket" "data_lake_prod" {
# ... bucket config ...
lifecycle {
prevent_destroy = true # terraform destroy fails with an error
# to actually destroy: remove this block, plan, review, apply
}
}
resource "snowflake_database" "freshcart_prod" {
name = "FRESHCART_PROD"
lifecycle { prevent_destroy = true }
}prevent_destroy on anything genuinely catastrophic to lose.Five Misconceptions About Infrastructure as Code
Onboarding a New Data Engineer in 30 Minutes With IaC
Marcus Bennett joins FreshCart as a data engineer. Before IaC, onboarding took 3-5 days: manually creating an S3 prefix, requesting Snowflake access from IT, waiting for IAM role creation, configuring dbt profiles with manual credential lookup. With IaC, the entire environment is ready in 30 minutes with one PR.
# terraform/environments/dev/main.tf
module "snowflake_dev" {
source = "../../modules/snowflake_env"
environment = "dev"
analysts = [
"jenna@freshcart.com",
"marcus.bennett@freshcart.com", # ← ADD THIS LINE
]
}
# modules/s3_developer_access/main.tf
resource "aws_iam_policy" "dev_s3_access" {
for_each = toset(var.developer_emails)
name = "freshcart-dev-${replace(each.value, "@freshcart.com", "")}-s3"
policy = jsonencode({
Version = "2012-10-17"
Statement = [{
Effect = "Allow"
Action = ["s3:GetObject", "s3:PutObject", "s3:ListBucket"]
Resource = ["arn:aws:s3:::freshcart-dev-data-lake/dev/${replace(each.value, "@freshcart.com", "")}/*"]
}]
})
}Plan: 3 to add, 0 to change, 0 to destroy.
+ snowflake_user.analysts["marcus.bennett@freshcart.com"]
+ snowflake_role_grants.analysts["marcus.bennett@freshcart.com"]
+ aws_iam_policy.dev_s3_access["marcus.bennett@freshcart.com"]
# PR reviewed and merged → terraform apply runs:
# → Snowflake user created, temp password, MUST_CHANGE_PASSWORD=true
# → IAM policy created and attached
Marcus's checklist (30 minutes total):
[x] PR merged — Snowflake + AWS access provisioned automatically
[x] Receives temp Snowflake password (forced change on first login)
[x] Clones the dbt repo, runs: dbt run --target dev --select +silver.orders
[x] Queries his dev schema in Snowflake — data there immediatelyContrast with the manual process this replaced: a Jira ticket to IT on day 1, a follow-up on day 2, a Snowflake user created with the wrong role on day 3, an AWS access request form on day 4, and dbt setup finally working on day 5 — five days, six Slack messages, two tickets, one frustrated engineer. Offboarding Marcus later is just as simple: remove his email from the list, merge, and every piece of access is revoked in one automated step.
5 Interview Questions — With Complete Answers
Mistakes Beginners Make Constantly
Errors You Will Hit — And Exactly Why They Happen
🎯 Key Takeaways
- ✓Infrastructure as Code treats cloud resources — S3 buckets, Snowflake warehouses, IAM roles — like software: defined in version-controlled files, reviewed in PRs, deployed through CI/CD. The benefits: reproducibility, auditability, drift prevention, cost visibility, security by default, and environment parity between dev and prod.
- ✓Terraform core workflow: init (download providers, initialise backend) → plan (show what will change, no changes made) → apply (make the changes). Always read the plan before apply. The three change types: + (create), ~ (update in-place), - (destroy). Any destroy operation requires deliberate review.
- ✓Terraform state maps resource blocks to real cloud resource IDs. Remote state (S3 + DynamoDB) is mandatory for teams: S3 provides durability and sharing, DynamoDB prevents concurrent applies from corrupting state. Never edit state manually. If state is lost: expensive to recover via terraform import.
- ✓Variables make Terraform reusable across environments. Use sensitive = true on credential variables — they are redacted in plan output (but still stored in plain text in state, which itself must be encrypted). Use validation blocks to enforce valid values. Use .tfvars files per environment to separate configuration from code.
- ✓Terraform modules encapsulate related resources as reusable components. A data_lake module wraps the S3 bucket, encryption, versioning, lifecycle policies, and access logging. A snowflake_env module wraps databases, schemas, roles, warehouses, and grants. Both prod and dev call the same module with different variable values — guaranteeing structural consistency.
- ✓S3 lifecycle policies must always specify a filter prefix. A lifecycle rule without a filter applies to ALL objects in the bucket. A 30-day deletion rule intended for landing/ applied without a prefix filter will delete all Silver and Gold data. Review every lifecycle rule in CI for a mandatory filter block.
- ✓The prevent_destroy lifecycle block prevents Terraform from destroying a critical resource. Terraform refuses to apply any plan that would destroy a resource with this flag. To remove a resource intentionally: remove the lifecycle block in a separate PR, review that intent explicitly, then delete. Apply this to all production databases, schemas, and S3 buckets.
- ✓IAM roles for data platforms follow least privilege: ingestion pipeline (write landing/bronze only), transformation pipeline (read bronze, write silver/gold), analyst (read silver/gold only, no bronze PII), BI service account (read gold only). Define every FUTURE GRANT in Terraform so new tables automatically inherit the correct permissions without manual grants.
- ✓Snowflake resource monitors set credit quotas per warehouse per month. Notify at 75% and 90%, suspend at 100%. Without a resource monitor, a runaway analyst query or a misconfigured pipeline can exhaust the entire monthly Snowflake compute budget in one day. Define resource monitors in Terraform so they are always present in all environments.
- ✓Onboarding a new engineer with IaC: add their email to the analysts variable list, open a PR, CI runs terraform plan showing the user creation, merge after review, Terraform provisions the Snowflake user with correct roles, IAM policies, and dev S3 access in minutes. Offboarding is the reverse: remove the email, PR, merge, access revoked automatically. Zero tickets, zero forgotten accounts.
What comes next
Module 46 covers data engineering system design — the complete framework for designing any data system from scratch, with five fully worked designs for scenarios you will encounter in senior interviews and real jobs.
Module 46 → Data Engineering System DesignDiscussion
0Have a better approach? Found something outdated? Share it — your knowledge helps everyone learning here.