Most people write Terraform for themselves first — state on the laptop, SA with Owner, firewall on 0.0.0.0/0. The pain shows up the day someone else has to take over, or the security team runs a scan.
This project starts from “assume it will be audited”, in ~140 lines. Three details are worth lifting.
1. Remote state without a hardcoded bucket
1 | backend "gcs" { |
bucket is deliberately missing. It is injected at init:
1 | terraform init -backend-config="bucket=$TFSTATE_BUCKET" |
Why:
- Same code can target dev and prod state with no source change
- A public repo doesn’t leak the state bucket name
- Onboarding a new engineer needs one env var, not a story
2. A least-privilege service account
1 | resource "google_service_account" "airflow_sa" { |
Key choices:
bigquery.dataEditor+bigquery.jobUserinstead ofbigquery.admin: can read/write tables and run jobs, cannot delete the dataset or change IAM.- GCS permission is bucket-scoped, not project-scoped. A new bucket added to the project is invisible to this SA by default.
- The VM attaches the SA directly — no key files. Key files are the single highest-probability leak vector.
1 | service_account { |
3. Firewall: UI only reachable from your own IP
1 | resource "google_compute_firewall" "airflow_ui" { |
admin_cidr is a required variable (no default in variables.tf). Every apply must pass it explicitly:
1 | terraform apply -var="admin_cidr=203.0.113.4/32" |
Why required? Because anything with a default eventually gets quietly changed to 0.0.0.0/0. Removing the dangerous default at the language level beats relying on code review.
Anti-pattern cheat sheet
| Anti-pattern | What this project does |
|---|---|
| Local state file | GCS backend with injected bucket |
SA with Owner / Editor |
dataEditor + jobUser + bucket-level objectAdmin |
| Ship SA key to the VM | VM attaches SA, no key |
source_ranges = ["0.0.0.0/0"] |
Required admin_cidr variable |
force_destroy = true |
Defaults to false, gated behind allow_destroy |
Short file, but every line maps to a real incident someone has lived through.