Tools: Automatizando FinOps na AWS: Como construímos um módulo de Monitoramento de Custos com Terraform e incident.io

Tools: Automatizando FinOps na AWS: Como construímos um módulo de Monitoramento de Custos com Terraform e incident.io

Source: Dev.to

O Problema ## A Arquitetura Simplificada ## A Implementação com Terraform ## 1. Segurança Primeiro: Criptografia KMS ## 2. O Tópico SNS e a política de Publicação ## 3. Flexibilidade com Dynamic Blocks ## 4. Integração via Webhook (A Parte "Chata") ## Conclusão Gerenciar custos em nuvem e um desafio constante para equipes de engenharia. A disciplina de FinOps busca trazer visibilidade e accountability para esses gastos, mas sem as ferramentas certas, e fácil ser surpreendido pela fatura no final do mês. Neste artigo, compartilho a experiência e os detalhes técnicos da criação de um módulo Terraform reutilizável para automatizar alertas de orçamento na AWS, integrando diretamente com ferramentas de resposta a incidentes (neste caso, o incident.io), eliminando intermediários desnecessários como funções Lambda para esse fim específico. Precisávamos de uma maneira padronizada de criar "guardrails" financeiros para novos projetos. Toda nova conta ou ambiente (staging, production) na AWS deveria nascer com um orçamento definido e um canal de alerta configurado. Inicialmente, considerou-se usar AWS Lambda para processar os alertas do AWS Budgets e formatar o JSON para o webhook. No entanto, percebemos que poderíamos simplificar a arquitetura utilizando a capacidade nativa do SNS de realizar chamadas HTTPS (Webhooks). O fluxo ficou assim: AWS Budgets -> SNS Topic (Criptografado) -> HTTP POST -> Incident.io Essa abordagem reduz a complexidade operacional (menos código para manter) e o custo. Abaixo, detalho as partes cruciais do módulo, focando em como resolvemos os desafios de segurança e flexibilidade. O AWS SNS suporta criptografia de dados (Server-Side Encryption). Para ambientes corporativos, isso não é opcional. No entanto, usar uma chave KMS gerenciada pelo cliente (CMK) requer politicas de acesso especificas para permitir que o serviço de AWS Budgets (que é um serviço global) chame um recurso regional e use a chave para criptografar a mensagem antes de enviá-la ao tópico. Observe a permissão explicita para budgets.amazonaws.com. Sem isso, o orçamento falha silenciosamente ao tentar publicar no tópico. O tópico SNS atua como o roteador. Além de criar o tópico, precisamos de uma aws_sns_topic_policy para autorizar o serviço de orçamento a publicar nele. Um dos maiores desafios ao criar módulos reutilizáveis e atender as diferentes necessidades de notificação de cada time. Alguns querem alertas aos 50%, outros apenas aos 100%. Utilizamos o recurso dynamic do Terraform para gerar as configurações de notificação com base em uma lista de objetos fornecida via variável. Isso permite que o consumidor do module configure alertas de forma declarativa e simples no arquivo .tfvars: A integração final e feita via uma assinatura SNS com protocolo HTTPS: O Desafio da Confirmação da Assinatura: Diferente de Lambda ou SQS, endpoints HTTP/HTTPS requerem uma confirmação de assinatura. O SNS envia um POST inicial com uma URL de confirmação. Como estamos usando uma ferramenta de terceiros (incident.io), não temos controle direto para clicar programaticamente nesse link. A solução adotada foi processual: Embora não seja 100% automatizado (zero-touch), e um compromisso aceitável para uma configuração que ocorre apenas uma vez por ambiente. Criar módulos de infraestrutura como código para FinOps e essencial para escalar o uso da nuvem de forma responsável. Ao centralizar a lógica de orçamentos, criptografia e notificações em um único modulo, garantimos que todos os ambientes seguem as melhores praticas de segurança e observabilidade financeira. A escolha de remover camadas intermediarias (como Lambdas) simplificou a stack, tornando-a mais robusta e fácil de manter a longo prazo. Templates let you quickly answer FAQs or store snippets for re-use. Are you sure you want to hide this comment? It will become hidden in your post, but will still be visible via the comment's permalink. Hide child comments as well For further actions, you may consider blocking this person and/or reporting abuse CODE_BLOCK: resource "aws_kms_key" "cost_alerts_key" { description = "KMS key for encrypting FinOps SNS topics" deletion_window_in_days = 10 enable_key_rotation = true policy = jsonencode({ Version = "2012-10-17" Statement = [ { Sid = "Enable IAM User Permissions" Effect = "Allow" Principal = { AWS = "arn:aws:iam::${local.account_id}:root" } Action = "kms:*" Resource = "*" }, { Sid = "Allow AWS Budgets to use the key" Effect = "Allow" Principal = { Service = "budgets.amazonaws.com" } Action = [ "kms:GenerateDataKey*", "kms:Decrypt" ] Resource = "*" } ] }) } Enter fullscreen mode Exit fullscreen mode CODE_BLOCK: resource "aws_kms_key" "cost_alerts_key" { description = "KMS key for encrypting FinOps SNS topics" deletion_window_in_days = 10 enable_key_rotation = true policy = jsonencode({ Version = "2012-10-17" Statement = [ { Sid = "Enable IAM User Permissions" Effect = "Allow" Principal = { AWS = "arn:aws:iam::${local.account_id}:root" } Action = "kms:*" Resource = "*" }, { Sid = "Allow AWS Budgets to use the key" Effect = "Allow" Principal = { Service = "budgets.amazonaws.com" } Action = [ "kms:GenerateDataKey*", "kms:Decrypt" ] Resource = "*" } ] }) } CODE_BLOCK: resource "aws_kms_key" "cost_alerts_key" { description = "KMS key for encrypting FinOps SNS topics" deletion_window_in_days = 10 enable_key_rotation = true policy = jsonencode({ Version = "2012-10-17" Statement = [ { Sid = "Enable IAM User Permissions" Effect = "Allow" Principal = { AWS = "arn:aws:iam::${local.account_id}:root" } Action = "kms:*" Resource = "*" }, { Sid = "Allow AWS Budgets to use the key" Effect = "Allow" Principal = { Service = "budgets.amazonaws.com" } Action = [ "kms:GenerateDataKey*", "kms:Decrypt" ] Resource = "*" } ] }) } CODE_BLOCK: resource "aws_sns_topic" "cost_alerts" { name_prefix = "${local.name_prefix}-cost-alerts-" kms_master_key_id = aws_kms_key.cost_alerts_key.arn } resource "aws_sns_topic_policy" "default" { arn = aws_sns_topic.cost_alerts.arn policy = jsonencode({ Version = "2012-10-17" Statement = [ { Sid = "AWSBudgets-Publish" Effect = "Allow" Principal = { Service = "budgets.amazonaws.com" } Action = "SNS:Publish" Resource = aws_sns_topic.cost_alerts.arn } ] }) } Enter fullscreen mode Exit fullscreen mode CODE_BLOCK: resource "aws_sns_topic" "cost_alerts" { name_prefix = "${local.name_prefix}-cost-alerts-" kms_master_key_id = aws_kms_key.cost_alerts_key.arn } resource "aws_sns_topic_policy" "default" { arn = aws_sns_topic.cost_alerts.arn policy = jsonencode({ Version = "2012-10-17" Statement = [ { Sid = "AWSBudgets-Publish" Effect = "Allow" Principal = { Service = "budgets.amazonaws.com" } Action = "SNS:Publish" Resource = aws_sns_topic.cost_alerts.arn } ] }) } CODE_BLOCK: resource "aws_sns_topic" "cost_alerts" { name_prefix = "${local.name_prefix}-cost-alerts-" kms_master_key_id = aws_kms_key.cost_alerts_key.arn } resource "aws_sns_topic_policy" "default" { arn = aws_sns_topic.cost_alerts.arn policy = jsonencode({ Version = "2012-10-17" Statement = [ { Sid = "AWSBudgets-Publish" Effect = "Allow" Principal = { Service = "budgets.amazonaws.com" } Action = "SNS:Publish" Resource = aws_sns_topic.cost_alerts.arn } ] }) } COMMAND_BLOCK: resource "aws_budgets_budget" "cost_budget" { name = "${local.name_prefix}-budget" budget_type = "COST" limit_amount = var.limit_amount limit_unit = var.limit_unit time_period_start = "2024-01-01_00:00" time_unit = var.time_unit # Bloco dinamico para notificacoes dynamic "notification" { for_each = var.notifications content { comparison_operator = notification.value.comparison_operator threshold = notification.value.threshold threshold_type = notification.value.threshold_type notification_type = notification.value.notification_type subscriber_sns_topic_arns = [aws_sns_topic.cost_alerts.arn] } } } Enter fullscreen mode Exit fullscreen mode COMMAND_BLOCK: resource "aws_budgets_budget" "cost_budget" { name = "${local.name_prefix}-budget" budget_type = "COST" limit_amount = var.limit_amount limit_unit = var.limit_unit time_period_start = "2024-01-01_00:00" time_unit = var.time_unit # Bloco dinamico para notificacoes dynamic "notification" { for_each = var.notifications content { comparison_operator = notification.value.comparison_operator threshold = notification.value.threshold threshold_type = notification.value.threshold_type notification_type = notification.value.notification_type subscriber_sns_topic_arns = [aws_sns_topic.cost_alerts.arn] } } } COMMAND_BLOCK: resource "aws_budgets_budget" "cost_budget" { name = "${local.name_prefix}-budget" budget_type = "COST" limit_amount = var.limit_amount limit_unit = var.limit_unit time_period_start = "2024-01-01_00:00" time_unit = var.time_unit # Bloco dinamico para notificacoes dynamic "notification" { for_each = var.notifications content { comparison_operator = notification.value.comparison_operator threshold = notification.value.threshold threshold_type = notification.value.threshold_type notification_type = notification.value.notification_type subscriber_sns_topic_arns = [aws_sns_topic.cost_alerts.arn] } } } CODE_BLOCK: notifications = [ { comparison_operator = "GREATER_THAN" threshold = 80 threshold_type = "PERCENTAGE" notification_type = "ACTUAL" }, { comparison_operator = "GREATER_THAN" threshold = 100 threshold_type = "PERCENTAGE" notification_type = "FORECASTED" } ] Enter fullscreen mode Exit fullscreen mode CODE_BLOCK: notifications = [ { comparison_operator = "GREATER_THAN" threshold = 80 threshold_type = "PERCENTAGE" notification_type = "ACTUAL" }, { comparison_operator = "GREATER_THAN" threshold = 100 threshold_type = "PERCENTAGE" notification_type = "FORECASTED" } ] CODE_BLOCK: notifications = [ { comparison_operator = "GREATER_THAN" threshold = 80 threshold_type = "PERCENTAGE" notification_type = "ACTUAL" }, { comparison_operator = "GREATER_THAN" threshold = 100 threshold_type = "PERCENTAGE" notification_type = "FORECASTED" } ] COMMAND_BLOCK: resource "aws_sns_topic_subscription" "incident_io" { topic_arn = aws_sns_topic.cost_alerts.arn protocol = "https" # Detectado dinamicamente no codigo real endpoint = var.incident_io_webhook_url } Enter fullscreen mode Exit fullscreen mode COMMAND_BLOCK: resource "aws_sns_topic_subscription" "incident_io" { topic_arn = aws_sns_topic.cost_alerts.arn protocol = "https" # Detectado dinamicamente no codigo real endpoint = var.incident_io_webhook_url } COMMAND_BLOCK: resource "aws_sns_topic_subscription" "incident_io" { topic_arn = aws_sns_topic.cost_alerts.arn protocol = "https" # Detectado dinamicamente no codigo real endpoint = var.incident_io_webhook_url } - Definir um limite mensal em dólares. - Alertar quando o gasto real atingir certas porcentagens (ex: 80%, 100%). - Alertar quando a previsão (forecast) indicar que o orçamento será estourado. - Enviar esses alertas para nossa plataforma de gerenciamento de incidentes. - Ser seguro (dados criptografados em repouso). - O Terraform aplica a infraestrutura. - O estado da assinatura fica como "PendingConfirmation". - O administrador acessa os logs do incident.io, localiza a mensagem de SubscriptionConfirmation e clica no link manualmente.