Separar dev de prod na GCP tem duas abordagens principais: projetos GCP distintos ou workspaces do Terraform. A escolha muda radicalmente o isolamento real de recursos, IAM e cobrança.

Projetos separados (recomendado para dados)

Cada ambiente tem seu próprio projeto GCP. Billing, IAM, quotas e limites de API são isolados por padrão. Um bug em dev não pode consumir as quotas de prod nem afetar recursos do ambiente de produção.

organização/
├── meu-projeto-dev   (projeto GCP isolado)
└── meu-projeto-prod  (projeto GCP isolado)

Estrutura de arquivos para essa abordagem:

infra/
├── modules/
│   ├── storage/
│   │   └── main.tf
│   ├── bigquery/
│   │   └── main.tf
│   └── looker/
│       └── main.tf
├── envs/
│   ├── dev/
│   │   ├── main.tf           ← chama os módulos
│   │   ├── variables.tf
│   │   ├── terraform.tfvars
│   │   └── backend.tf
│   └── prod/
│       ├── main.tf
│       ├── variables.tf
│       ├── terraform.tfvars
│       └── backend.tf
└── schemas/
    └── pedidos.json          ← schemas BigQuery compartilhados

O backend.tf de cada ambiente aponta para um bucket de state distinto:

# envs/dev/backend.tf
terraform {
  backend "gcs" {
    bucket = "meu-projeto-tfstate-dev"
    prefix = "infra/terraform.tfstate"
  }
}
 
# envs/prod/backend.tf
terraform {
  backend "gcs" {
    bucket = "meu-projeto-tfstate-prod"
    prefix = "infra/terraform.tfstate"
  }
}

Workspaces (adequado para ambientes simples)

Workspaces compartilham o mesmo projeto GCP. Adequado quando isolamento de IAM e billing não é crítico (times pequenos, workloads não-regulados).

locals {
  ambiente = terraform.workspace  # "dev" ou "prod"
  prefixo  = "meu-projeto-${local.ambiente}"
 
  config = {
    dev = {
      bigquery_edition     = "STANDARD"
      bq_delete_on_destroy = true
      bq_table_expiration  = 2592000000  # 30d em ms
      slot_count           = 0
    }
    prod = {
      bigquery_edition     = "ENTERPRISE"
      bq_delete_on_destroy = false
      bq_table_expiration  = null
      slot_count           = 100
    }
  }
 
  cfg = local.config[local.ambiente]
}
terraform workspace new dev
terraform workspace select dev
terraform apply -var-file="dev.tfvars"
 
terraform workspace new prod
terraform workspace select prod
terraform apply -var-file="prod.tfvars"

Variáveis por ambiente

# variables.tf (compartilhado entre envs)
variable "projeto_id" { type = string }
variable "regiao"     { type = string }
variable "ambiente"   { type = string }
variable "prefixo"    { type = string }
 
variable "bigquery_edition" {
  type        = string
  description = "Edição do BigQuery Reservations. Valores: STANDARD, ENTERPRISE, ENTERPRISE_PLUS"
  default     = "STANDARD"
}
# dev.tfvars
projeto_id       = "meu-projeto-dev"
regiao           = "southamerica-east1"
ambiente         = "dev"
prefixo          = "meu-projeto-dev"
bigquery_edition = "STANDARD"
 
# prod.tfvars
projeto_id       = "meu-projeto-prod"
regiao           = "southamerica-east1"
ambiente         = "prod"
prefixo          = "meu-projeto-prod"
bigquery_edition = "ENTERPRISE"

Labels obrigatórias

Labels são a base para filtros de custo no Billing e auditoria de recursos. Todo recurso criado deve receber o bloco:

locals {
  labels = {
    ambiente  = var.ambiente   # "dev" | "prod"
    projeto   = var.prefixo
    managedby = "terraform"
    time      = "dados"        # identifica o time responsável
  }
}

GCS, BigQuery, Compute e a maioria dos recursos GCP usam o campo labels (não tags). O nome é consistente no provider Google.

Habilitando APIs por ambiente

APIs precisam ser habilitadas uma vez por projeto. Use google_project_service para garantir isso via código:

resource "google_project_service" "apis" {
  for_each = toset([
    "bigquery.googleapis.com",
    "storage.googleapis.com",
    "looker.googleapis.com",
    "bigqueryreservation.googleapis.com",
    "bigqueryconnection.googleapis.com",
  ])
 
  service            = each.value
  disable_on_destroy = false
}

disable_on_destroy = false é importante: se outro time usa a mesma API no projeto, desabilitá-la ao destruir o state quebraria os recursos deles.

Service Accounts por ambiente

Crie SAs com escopo mínimo para cada serviço. Nunca use a SA padrão do projeto em workloads de dados:

resource "google_service_account" "pipeline" {
  account_id   = "${local.prefixo}-pipeline"
  display_name = "Pipeline de Dados - ${var.ambiente}"
}
 
# Permissão mínima: BigQuery Job User (para executar queries)
resource "google_project_iam_member" "pipeline_bq_job" {
  project = var.projeto_id
  role    = "roles/bigquery.jobUser"
  member  = "serviceAccount:${google_service_account.pipeline.email}"
}

Promoção dev → prod

# 1. Apply em dev
cd envs/dev
terraform plan -out=dev.plan
terraform apply dev.plan
 
# 2. Validação: smoke tests, dados de exemplo, checks de schema...
 
# 3. Apply em prod (mesmo código, tfvars diferente)
cd ../prod
terraform plan -out=prod.plan
terraform apply prod.plan

O gap entre os dois apply é o gate de qualidade. Nenhuma mudança de infraestrutura vai para prod sem ter sido aplicada e validada em dev primeiro.

Comparação das abordagens

CritérioProjetos separadosWorkspaces
Isolamento de billingTotalNenhum
Isolamento de IAMTotalParcial (mesmas SAs)
Isolamento de quotasTotalNenhum
Complexidade de setupMaiorMenor
Risco de blast radiusBaixoMédio
Adequado paraDados regulados, times maioresProjetos simples, prototipação

Conexões

Ver também: terraform-cloud-gcp | terraform-variaveis | terraform-state | terraform-gcp-buckets | terraform-gcp-bigquery | terraform-gcp-looker