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
}
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
}
⚠️ Gotcha: Different GCP resources use different label attribute names —
labels,user_labels, orresource_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."
}
}
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."
📊 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;
This single query has helped teams discover:
- 🔥 A
stagingenvironment costing 40% of prod because nobody scaled it down - 👻 Resources labeled
team: former-intern-projectstill 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
}
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
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)