De nada adianta ter um budget e um export de custo se ninguém sabe de qual time é cada Resource Group. É pra isso que serve a Governança de Tags no Azure com Terraform: uma Azure Policy que adiciona automaticamente a tag CostCenter em todo Resource Group novo, sem depender de disciplina manual — usando o efeito modify e o recurso azurerm_policy_definition.
Este é o terceiro artigo da série de FinOps no Azure. Além do módulo Terraform, você vai ver dois bugs reais que apareceram no caminho — um erro de sintaxe da policy e uma causa raiz nada óbvia que fazia a tag nunca ser aplicada — e como cada um foi diagnosticado e corrigido.
Segurança: os comandos deste guia de Governança de Tags no Azure com Terraform utilizam variáveis de ambiente para credenciais. Nunca insira IDs de subscription, senhas ou chaves diretamente nos comandos — use TF_VAR_*, um arquivo .tfvars fora do Git, ou o Azure Key Vault.
Sumário
- Pré-requisitos
- Modify vs. DeployIfNotExists
- Criando o módulo terraform-tag-governance-modules
- Instanciando o módulo
- terraform plan e apply
- Confirmando a Governança de Tags no Azure com Terraform
- Troubleshooting: dois bugs reais
- Conclusão
- Referências oficiais
Pré-requisitos
Antes de criar a Governança de Tags no Azure com Terraform, você vai precisar do Azure CLI autenticado (az login), do Terraform >= 1.5 e de uma role com permissão para criar Policy Definitions, Policy Assignments e Role Assignments na subscription (Resource Policy Contributor + User Access Administrator, ou Owner). O provider usado nesta Governança de Tags no Azure com Terraform é o azurerm ~> 4.0.
az account show --query "{name:name, id:id}" -o table
Modify vs. DeployIfNotExists
Antes de sair aplicando, vale entender qual efeito de Azure Policy usar. Se você leu o artigo sobre Azure Policy DeployIfNotExists, talvez pense em usar o mesmo efeito nesta Governança de Tags no Azure com Terraform. Não é o caso: deployIfNotExists dispara um deployment ARM separado, assíncrono, depois que o recurso já existe — ótimo pra provisionar um recurso dependente (como um agente de monitoramento). Para adicionar ou corrigir uma tag, o efeito correto é modify: ele altera a própria requisição de criação/atualização do recurso, em tempo real, antes dela ser persistida. É o mesmo efeito usado pelas políticas built-in da Microsoft como “Inherit a tag from the subscription if missing”.
Criando o módulo terraform-tag-governance-modules
O módulo da Governança de Tags no Azure com Terraform encapsula três recursos: a definição da policy, a atribuição na subscription (com managed identity) e a role assignment que dá à identidade permissão para escrever a tag — toda a Governança de Tags no Azure com Terraform cabe nesses três blocos:
# main.tf
resource "azurerm_policy_definition" "tag_governance" {
name = var.policy_name
policy_type = "Custom"
mode = "All"
display_name = var.policy_display_name
policy_rule = jsonencode({
if = {
field = "type"
equals = "Microsoft.Resources/subscriptions/resourceGroups"
}
then = {
effect = "modify"
details = {
roleDefinitionIds = [
"/providers/microsoft.authorization/roleDefinitions/4a9ae827-6dc8-4573-8ac7-8239d42aa03f" # Tag Contributor
]
operations = [
{
operation = "add"
field = "tags['${var.tag_name}']"
value = var.tag_default_value
}
]
}
}
})
}
resource "azurerm_subscription_policy_assignment" "tag_governance" {
name = var.assignment_name
display_name = var.policy_display_name
policy_definition_id = azurerm_policy_definition.tag_governance.id
subscription_id = "/subscriptions/${var.subscription_id}"
location = var.location
identity {
type = "SystemAssigned"
}
}
resource "azurerm_role_assignment" "tag_contributor" {
scope = "/subscriptions/${var.subscription_id}"
role_definition_name = "Tag Contributor"
principal_id = azurerm_subscription_policy_assignment.tag_governance.identity[0].principal_id
}
Repare em dois detalhes que quase sempre passam despercebidos e que causaram os dois bugs deste artigo: a operação é "add" (não "addOrReplace" com uma condição manual), e o mode é "All" — mais adiante você vai ver exatamente o que acontece quando qualquer um dos dois está errado.
# variables.tf
variable "subscription_id" { type = string } # sensitive
variable "location" { type = string; default = "eastus" }
variable "policy_name" { type = string; default = "tag-governance-cost-center" }
variable "policy_display_name" { type = string; default = "Adicionar tag CostCenter em Resource Groups" }
variable "assignment_name" { type = string; default = "assign-tag-governance-cost-center" }
variable "tag_name" { type = string; default = "CostCenter" }
variable "tag_default_value" { type = string; default = "unassigned" }
Instanciando o módulo
O ambiente que efetivamente cria a Governança de Tags no Azure com Terraform só passa a subscription e a região da managed identity — nada de hardcode de valores específicos, tudo vem do módulo:
module "tag_governance" {
source = "../../terraform-tag-governance-modules"
subscription_id = var.subscription_id
location = var.location
}
O subscription_id nunca vai hardcoded — é passado via variável marcada como sensitive = true e populada por TF_VAR_subscription_id no ambiente de execução, nunca commitado:
export TF_VAR_subscription_id="<SUBSCRIPTION_ID>"
terraform plan e apply
Com o backend remoto inicializado, o plan dessa Governança de Tags no Azure com Terraform mostra os três recursos a criar — nada de destruir ou recriar nada que já existe:
$ terraform plan -out=tfplan.out
# module.tag_governance.azurerm_policy_definition.tag_governance will be created
# module.tag_governance.azurerm_subscription_policy_assignment.tag_governance will be created
# module.tag_governance.azurerm_role_assignment.tag_contributor will be created
Plan: 3 to add, 0 to change, 0 to destroy.
Aprovado o plano, o apply cria os três recursos da Governança de Tags no Azure com Terraform:
$ terraform apply tfplan.out
module.tag_governance.azurerm_policy_definition.tag_governance: Creation complete after 1m41s
module.tag_governance.azurerm_subscription_policy_assignment.tag_governance: Creation complete after 1m51s
module.tag_governance.azurerm_role_assignment.tag_contributor: Creation complete after 29s
Apply complete! Resources: 3 added, 0 changed, 0 destroyed.
Confirmando a Governança de Tags no Azure com Terraform
No portal, a policy custom aparece em Policy → Definitions, e a atribuição em Policy → Assignments, já com a managed identity criada:


A prova real: um Resource Group criado do zero, sem passar nenhuma tag, já nasce com CostCenter: unassigned aplicado automaticamente pela policy:

Sobre a Governança de Tags no Azure com Terraform, é importante deixar claro: o efeito modify age no momento da requisição — ele não é retroativo. Resource Groups que já existiam antes da policy ser atribuída continuam sem a tag até você rodar uma remediation task (o mesmo mecanismo usado no artigo sobre Boot Diagnostics) ou atualizá-los manualmente.
Troubleshooting: dois bugs reais
Montar essa Governança de Tags no Azure com Terraform não funcionou de primeira — dois erros reais apareceram, documentados aqui porque nenhum dos dois é óbvio pela mensagem de erro (ou pela ausência dela):
Bug 1 — InvalidModifyOperationCondition
A primeira versão da operation usava addOrReplace com uma condição manual pra só agir se a tag estivesse ausente:
operations = [
{
operation = "addOrReplace"
field = "tags['CostCenter']"
value = "unassigned"
condition = "[not(contains(field('tags'), 'CostCenter'))]"
}
]
O terraform apply falhou imediatamente na criação da policy definition:
Error: creating/updating Policy Definition "tag-governance-cost-center":
policy.DefinitionsClient#CreateOrUpdate: Status=400
Code="InvalidModifyOperationCondition"
Message="...has unsupported functions: 'FIELD'. Modify operation conditions
are not allowed to have the following functions: 'RESOURCEGROUP,SUBSCRIPTION,
POLICY,FIELD,CURRENT,CLAIMS,CAPABILITY,POLICYASSIGNMENT'"
A função field() simplesmente não é permitida dentro de condition em operations de modify. A correção é mais simples do que o problema parecia: trocar addOrReplace + condição manual pela operação add, que por definição só atua se o campo ainda não existir — sem precisar de nenhuma condição:
operations = [
{
operation = "add"
field = "tags['CostCenter']"
value = "unassigned"
}
]
Bug 2 — a tag nunca era aplicada (sem nenhum erro)
Com o Bug 1 corrigido, o apply passou limpo: policy definition, assignment e role assignment criados com sucesso, sem erro nenhum. Só que a Governança de Tags no Azure com Terraform continuava sem efeito nenhum — Resource Groups novos nasciam sem a tag CostCenter, sem nenhuma mensagem de erro, nenhum indício óbvio do que estava errado.
A primeira suspeita foi propagação de Azure AD (a managed identity de uma policy pode levar até 30-40 minutos para a role assignment funcionar de fato). Depois de esperar bem além desse tempo, com a role assignment confirmada correta via az role assignment list, o problema persistia — descartando propagação como causa.
A causa raiz dessa falha silenciosa na Governança de Tags no Azure com Terraform estava no mode da policy definition, que tinha sido deixado como "Indexed":
resource "azurerm_policy_definition" "tag_governance" {
mode = "Indexed" # ← errado para esse caso
# ...
}
Indexed é o modo recomendado (e mais comum) para políticas que avaliam recursos comuns — mas ele não avalia o próprio Resource Group como um recurso, só os recursos criados dentro dele. Como a regra desta policy usa if.type == Microsoft.Resources/subscriptions/resourceGroups, ou seja, mira no Resource Group em si, o modo precisa ser "All":
resource "azurerm_policy_definition" "tag_governance" {
mode = "All" # avalia Resource Groups, Subscriptions e recursos
# ...
}
Depois do terraform apply com a correção (um simples update in-place, sem recriar nada), um Resource Group novo já nasceu com a tag aplicada — foi o print de validação mostrado na seção anterior.
A tabela resume os dois bugs enfrentados nesta Governança de Tags no Azure com Terraform:
| Sintoma | Causa | Solução |
|---|---|---|
InvalidModifyOperationCondition no apply | field() não é permitido dentro de condition em operations de modify | Trocar addOrReplace + condição manual pela operação add, que só atua se o campo não existir |
| Apply passa limpo, mas a tag nunca aparece em RGs novos | mode = "Indexed" não avalia o próprio Resource Group como recurso | Trocar para mode = "All" |
Conclusão
Budget avisa quando o custo estoura, o export dá o histórico pra investigar — mas nenhum dos dois funciona direito sem saber de quem é cada recurso. A Governança de Tags no Azure com Terraform fecha esse ciclo garantindo que a tag de rateio nasce junto com o Resource Group, sem depender de ninguém lembrar de preencher. Os dois bugs documentados aqui — a sintaxe de condition em modify e a pegadinha do mode: Indexed vs. All — são exatamente o tipo de detalhe que a documentação não deixa óbvio até você bater de frente com ele.
Referências oficiais
Documentação oficial usada como base para este guia de Governança de Tags no Azure com Terraform, caso você queira se aprofundar no efeito modify e nos modos de avaliação da Azure Policy:
- azurerm_policy_definition (Terraform Registry)
- Understand Azure Policy effects — modify (Microsoft Learn)
- Azure Policy definition structure — modes (Microsoft Learn)
Interessado em saber mais sobre artigos relacionados ao Microsoft Azure CLIQUE AQUI
🚀 Vamos nos conectar?
Não perca nenhuma oportunidade! Cadastre-se nas minhas redes e no canal do YouTube para receber conteúdos de TI, Cloud, Azure, Kubernetes e DevOps em primeira mão.
Dica: No Facebook, todos os artigos do blog são publicados automaticamente. Vale a pena curtir!
💬 Dúvidas ou Problemas?
Com o intuito de ajudar a comunidade, caso você tenha dúvidas ou encontre problemas na execução dos comandos deste artigo, deixe um comentário abaixo. Responderei o mais breve possível!
Muito obrigado pela visita e até o próximo post!
Jefferson Castilho Especialista em Cloud & DevOps.Este guia técnico é exclusivo do Blog do Castilho. Explore mais conteúdos sobre Cloud e DevOps.