DEV Community

Cover image for Label Everything or Lose Everything: The #1 GCP Cost Move You're Probably Skipping 🏷️
Suhas Mallesh
Suhas Mallesh

Posted on

Label Everything or Lose Everything: The #1 GCP Cost Move You're Probably Skipping 🏷️

You're spending $50K/month on GCP. Your CFO asks: "Which team is burning the most? What's dev vs prod costing us?" You open the billing console and... you have no idea. Welcome to the unlabeled cloud — where money vanishes and nobody knows who spent what.

Before we optimize a single VM or right-size a single database in this series, we need to fix the foundation: labels.

Without labels, your GCP billing data is a giant blob. You see totals per service, maybe per project, but you can't slice by team, environment, application, or cost center. Every optimization we do in this series depends on answering one question: "Who's spending what?"

Labels answer that. Let's set them up right.

📊 The 5 Labels That Cover 90% of Cost Allocation

Don't overcomplicate this. Google recommends no more than 10 label keys. Here's the battle-tested set:

Label Key Example Values Purpose
env dev, staging, prod Environment separation
team platform, data-eng, ml Team cost ownership
app payment-api, user-svc Application attribution
cost-center cc-1234, cc-5678 Finance mapping
managed-by terraform, manual IaC tracking

⚠️ GCP enforces lowercase with only letters, numbers, hyphens, and underscores for label keys and values. Each resource supports up to 64 labels.

🔧 Step 1: Define Labels Once, Apply Everywhere

The magic of Terraform — define labels in one place and every resource gets them automatically. No human error. No forgotten tags.

# labels.tf — single source of truth

locals {
  common_labels = {
    env         = var.environment
    team        = var.team
    app         = var.app_name
    cost-center = var.cost_center
    managed-by  = "terraform"
  }
}

variable "environment" {
  type = string
  validation {
    condition     = contains(["dev", "staging", "prod"], var.environment)
    error_message = "Environment must be dev, staging, or prod."
  }
}

variable "team" {
  type = string
}

variable "app_name" {
  type = string
}

variable "cost_center" {
  type = string
}
Enter fullscreen mode Exit fullscreen mode

Now apply to every resource:

# Compute Engine
resource "google_compute_instance" "app" {
  name         = "${var.app_name}-${var.environment}-vm"
  machine_type = "e2-medium"
  zone         = "us-central1-a"

  labels = local.common_labels

  boot_disk {
    initialize_params {
      image  = "debian-cloud/debian-12"
      labels = local.common_labels
    }
  }

  network_interface {
    network = "default"
  }
}

# Cloud Storage
resource "google_storage_bucket" "data" {
  name     = "${var.app_name}-${var.environment}-data"
  location = "US"
  labels   = local.common_labels
}

# Cloud SQL — uses `user_labels` not `labels` 🤦
resource "google_sql_database_instance" "db" {
  name             = "${var.app_name}-${var.environment}-db"
  database_version = "POSTGRES_15"
  region           = "us-central1"

  settings {
    tier        = "db-f1-micro"
    user_labels = local.common_labels  # 👈 Different attribute name!
  }
}

# GKE Cluster — uses `resource_labels` 🤦
resource "google_container_cluster" "primary" {
  name     = "${var.app_name}-${var.environment}-gke"
  location = "us-central1"

  resource_labels = local.common_labels  # 👈 Yet another attribute name!

  # ... other config
}
Enter fullscreen mode Exit fullscreen mode

⚠️ Gotcha: Different GCP resources use different label attribute names — labels, user_labels, or resource_labels. Always check the Terraform provider docs for your resource. Getting this wrong means silently unlabeled resources.

🛡️ Step 2: Enforce Labels So No One Skips Them

Labels are only useful if every resource has them. Here's how to block unlabeled deployments.

Option A: Terraform variable validation (start here)

# modules/labeled-instance/variables.tf

variable "labels" {
  type = map(string)
  validation {
    condition = alltrue([
      contains(keys(var.labels), "env"),
      contains(keys(var.labels), "team"),
      contains(keys(var.labels), "app"),
    ])
    error_message = "Labels must include: env, team, app."
  }
}
Enter fullscreen mode Exit fullscreen mode

Anyone using your module cannot run terraform apply without the required labels. Plan fails before anything gets created. 🚫

Option B: CI/CD pipeline gate

# .github/workflows/terraform.yml (snippet)
- name: Check for required labels
  run: |
    UNLABELED=$(terraform show -json plan.out | \
      jq -r '.resource_changes[] | 
        select(.change.after.labels == null or 
          (.change.after.labels | has("env","team","app") | not))
        | .address')
    if [ -n "$UNLABELED" ]; then
      echo "❌ Missing labels on: $UNLABELED" && exit 1
    fi
    echo "✅ All resources properly labeled."
Enter fullscreen mode Exit fullscreen mode

📊 Step 3: Turn Labels into Cost Insights

Here's the payoff. GCP billing export to BigQuery includes label data automatically — but only for resources that have labels.

Query: Cost breakdown by team and environment

SELECT
  (SELECT value FROM UNNEST(labels) WHERE key = 'team') AS team,
  (SELECT value FROM UNNEST(labels) WHERE key = 'env') AS environment,
  SUM(cost) + SUM(IFNULL(
    (SELECT SUM(c.amount) FROM UNNEST(credits) c), 0
  )) AS net_cost
FROM
  `your-project.billing_dataset.gcp_billing_export_v1_XXXXXX`
WHERE
  invoice.month = '202602'
GROUP BY 1, 2
ORDER BY net_cost DESC;
Enter fullscreen mode Exit fullscreen mode

This single query has helped teams discover:

  • 🔥 A staging environment costing 40% of prod because nobody scaled it down
  • 👻 Resources labeled team: former-intern-project still running 6 months later
  • 💸 Dev environments running 24/7 when they're only used 8 hours a day

Each of these becomes a direct savings action in future posts.

🏢 Step 4: Label Your Projects Too

Project-level labels are especially valuable for FinOps teams doing org-wide analysis across hundreds of projects:

resource "google_project" "my_project" {
  name       = "My App - ${var.environment}"
  project_id = "myapp-${var.environment}"
  org_id     = var.org_id

  labels = local.common_labels
}
Enter fullscreen mode Exit fullscreen mode

This lets you run billing queries across your entire organization grouped by team, cost center, or environment — without knowing which project belongs to which team.

💡 Quick Reference: What to Do First

Action Effort Impact
Add locals block with common labels 5 min Foundation for all cost visibility
Apply labels to existing Terraform resources 30 min Instant billing attribution
Add validation to shared modules 15 min Prevents future unlabeled resources
Set up billing export to BigQuery 20 min Unlocks cost analytics
Run first cost-by-label query 10 min Find your first hidden waste

Start with the locals block. It takes 5 minutes and everything else builds on it. 🎯

📊 TL;DR

No labels       = blind spending, zero accountability
With labels     = cost by team, env, app, cost-center
Terraform       = define once, apply everywhere, enforce always
BigQuery export = labels appear automatically in billing data
Different attrs = labels vs user_labels vs resource_labels (check docs!)
Enforce early   = variable validation + CI gates = no unlabeled resources
Enter fullscreen mode Exit fullscreen mode

Bottom line: You can't optimize what you can't see. Labels are the cheapest, fastest thing you can do in GCP — and they make every other optimization in this series 10x more effective. 🔍


Go add that locals block to your Terraform right now. It's 10 lines and turns your billing data from a black hole into a searchable goldmine. I'll wait. 😀

Found this helpful? Follow for more GCP cost optimization with Terraform! 💬

Top comments (0)