
Terraform for Data Teams: BigQuery, Cloud Run, and the Blueprint That Scales
Modules, workspaces, drift, and CI/CD — a working Terraform layout for teams shipping data platforms without becoming full-time infra engineers.
Data teams that manage BigQuery datasets, Cloud Run jobs, and service accounts through the console eventually paint themselves into a corner. A minimal, opinionated Terraform layout — one repo, modules per resource type, workspaces per environment, PR-based CI — carries you from 5 to 500 pipelines without a platform team.
- You don't need a platform team to benefit from IaC — you need a small, opinionated layout you actually follow.
- One monorepo per data platform beats one repo per pipeline for a team of under 20.
- Workspaces separate environments (dev/stage/prod) with the same code and different state.
- Drift is the enemy — enable Terraform Cloud drift detection or a nightly plan job.
- Never store credentials in state; use workload identity or short-lived tokens.
The click-ops trap
Data teams start by clicking through the console — create a dataset, grant an IAM role, deploy a Cloud Run job. It works, until six months later nobody remembers who created what, staging and prod have drifted, and the on-call engineer has no idea which service accounts can access which datasets.
Terraform is not about being fancy — it's about having one place where the truth lives. Every dataset, table, service account, IAM binding, Cloud Run job, and pub/sub topic in one repo, reviewed via PR, applied via CI.
A minimal working layout
terraform/
├── modules/
│ ├── bigquery_dataset/ # reusable module with variables
│ ├── cloud_run_job/
│ ├── service_account/
│ └── pubsub_topic/
├── envs/
│ ├── dev/
│ │ ├── main.tf # instantiates modules for dev
│ │ ├── backend.tf # remote state config
│ │ └── terraform.tfvars
│ ├── stage/
│ └── prod/
└── shared/ # cross-env resources (DNS, org policies)This layout scales from one engineer to a team of twenty. Each environment has its own state file, but shares the same modules. A change to modules/bigquery_dataset is applied to all envs on merge, environment-by-environment via CI.
A dataset module you can copy
# modules/bigquery_dataset/main.tf
resource "google_bigquery_dataset" "this" {
dataset_id = var.dataset_id
location = var.location
default_table_expiration_ms = var.default_expiration_ms
labels = merge(var.labels, { managed_by = "terraform" })
}
resource "google_bigquery_dataset_iam_binding" "readers" {
dataset_id = google_bigquery_dataset.this.dataset_id
role = "roles/bigquery.dataViewer"
members = var.reader_members
}
output "dataset_id" { value = google_bigquery_dataset.this.dataset_id }Notice: the module never reads secrets, never hard-codes members, always tags with managed_by. Those three habits prevent 80% of the operational pain.
Workspaces vs directories
Two ways to separate environments: Terraform workspaces (one dir, many state files) or directory-per-env (one dir per env, one state file each). I prefer directory-per-env for data teams because it's more explicit — a PR touching envs/prod is visibly a prod change and gets extra review.
Whichever you pick, keep it consistent across the repo. Mixed styles are worse than either style.
State: remote, encrypted, versioned
Local state is fine for a demo, disastrous for a team. Store state in GCS (or S3) with versioning and encryption enabled. Enable state locking (GCS does this automatically with a temporary lock object).
# envs/prod/backend.tf
terraform {
backend "gcs" {
bucket = "acme-terraform-state"
prefix = "data-platform/prod"
}
}CI/CD without a platform team
A pragmatic GitHub Actions workflow for a data team:
- On PR:
terraform planfor each affected env, post the plan output as a PR comment. - Require review from a data lead for any prod plan.
- On merge to main:
terraform applyagainst dev automatically. - Stage and prod apply are manual approvals (Environments in GitHub, or a separate workflow trigger).
- Nightly:
terraform planagainst all envs; if diff is non-empty, alert Slack (drift detection).
This gets you 95% of what Terraform Cloud offers without the license cost, and stays inside your existing dev workflow.
Credentials and identity
Never put a service account JSON key in the repo or in CI secrets. Use Workload Identity Federation to let GitHub Actions assume a GCP identity via OIDC. Rotate nothing, revoke via IAM, done.
For local development, use gcloud auth application-default login. Every engineer authenticates as themselves; the state doesn't contain any long-lived secrets.
Frequently asked
Terraform or Pulumi?
For data teams: Terraform. The Google provider is more mature, the community modules are richer, and HCL is easier to review than code that can do arbitrary things. Pulumi is stronger for teams already deep in TypeScript who need real control flow.
One repo or many?
One repo per data platform, up to about 20 engineers. Above that, split by domain (analytics platform vs ML platform vs streaming platform), not by resource type.
How do I handle secrets?
Store them in Secret Manager, reference them by name from Terraform (never by value), and let the runtime resolve them. Terraform state should never contain the raw secret.
Building something similar?
I help teams ship production-grade AI agents, n8n workflows, and data platforms. Let's talk about what you're building.
Work with me
Islam Gamal
AI, Data & Automation Engineer. I design and ship production AI agents, n8n workflows, and cloud data platforms — with a focus on reliability, cost, and measurable business impact. Founder of Tashghil and Tek bil Arabi.
More from Islam Gamal
Cloud Run + Cloud SQL: The Boring Blueprint That Runs Half My Production Systems
A complete production blueprint for Cloud Run + Cloud SQL on GCP — connection pooling, IAM, secrets, migrations, blue/green deploys, cost control, and the failure modes to prevent before they hit.
Self-Hosting n8n on Kubernetes: HA, Queue Mode, Autoscaling, and Backups That Actually Restore
The complete production playbook for n8n on Kubernetes — queue mode topology, KEDA autoscaling on Redis depth, Postgres HA and PITR, encryption-key rotation, zero-downtime upgrades, and the backup drill you must actually run.
The Modern Data Stack in 2026: What Actually Matters (and What to Skip)
A no-hype 30-minute tour of the 2026 data stack: ingestion, warehouse, transformation, orchestration, semantic layer, reverse ETL, observability, and the AI layer on top — with concrete tool picks, cost anchors, and the anti-patterns that quietly eat budgets.
Browse every article by Islam Gamal on the author page.