Existe uma correlação quase universal entre o crescimento de um sistema em produção e o crescimento de seus custos de infraestrutura. O que começa como algumas instâncias EC2 e um banco de dados RDS evolui, ao longo de meses, para dezenas de serviços, centenas de recursos e uma fatura AWS que surpreende até os mais experientes quando chega ao fim do mês.
A disciplina de FinOps — Financial Operations — trata a gestão de custos de cloud como uma responsabilidade de engenharia, não apenas financeira. O princípio central é que as pessoas que tomam decisões de arquitetura — engenheiros e arquitetos — devem ter visibilidade dos custos que essas decisões geram, e responsabilidade por otimizá-los. O FinOps não é sobre cortar custos a qualquer custo; é sobre maximizar o valor entregue por cada dólar gasto em infraestrutura.
Performance e custo estão intrinsecamente ligados: um sistema mal dimensionado paga pelo excesso de capacidade; um sistema sobrecarregado degrada a experiência do usuário e frequentemente gera custos de incidente superiores ao que seria economizado com dimensionamento correto. Otimizar os dois simultaneamente — entregando a performance que o negócio precisa pelo menor custo possível — é o objetivo desta disciplina.
Visibilidade de Custos com AWS Cost Explorer e Etiquetas
O primeiro passo de qualquer programa FinOps é visibilidade. Sem saber quanto cada parte do sistema custa, não é possível decidir onde otimizar.
Estratégia de Etiquetas para Alocação de Custos
# tags.tf — convenção de etiquetas para rastreamento de custos
locals {
tags_comuns = {
# Identificação do recurso
Project = var.project_name
Environment = var.environment
ManagedBy = "terraform"
# Alocação de custos
CostCenter = var.cost_center # ex: "eng-backend", "eng-plataforma"
Team = var.team # ex: "checkout", "catalog", "infra"
Owner = var.owner # email do responsável técnico
# Lifecycle
CreatedAt = timestamp()
ExpiresAt = var.expires_at # para recursos temporários
# Compliance
DataClass = var.data_class # "publico", "interno", "confidencial"
}
}
# Política SCP (Service Control Policy) que exige etiquetas obrigatórias
# Aplicada no nível de AWS Organization para todos os recursos
resource "aws_organizations_policy" "exigir_etiquetas" {
name = "ExigirEtiquetasObrigatorias"
type = "SERVICE_CONTROL_POLICY"
content = jsonencode({
Version = "2012-10-17"
Statement = [
{
Sid = "ExigirEtiquetaProject"
Effect = "Deny"
Action = [
"ec2:RunInstances",
"rds:CreateDBInstance",
"elasticache:CreateCacheCluster",
"eks:CreateCluster",
]
Resource = "*"
Condition = {
"Null" = {
"aws:RequestedRegion" = "false"
"aws:ResourceTag/Project" = "true"
}
}
}
]
})
}
Dashboard de Custos com AWS Cost Explorer
#!/bin/bash
# scripts/relatorio-custos.sh
# Gera relatório de custos por serviço e equipe para o mês atual
MES_INICIO=$(date +%Y-%m-01)
MES_FIM=$(date -d "$(date +%Y-%m-01) +1 month" +%Y-%m-%d 2>/dev/null || \
date -v+1m -v1d +%Y-%m-%d)
echo "=== Relatório de Custos AWS — $(date +%B/%Y) ==="
echo ""
# Custo total do mês
TOTAL=$(aws ce get-cost-and-usage \
--time-period Start=${MES_INICIO},End=${MES_FIM} \
--granularity MONTHLY \
--metrics BlendedCost \
--query 'ResultsByTime[0].Total.BlendedCost.Amount' \
--output text)
echo "💰 Custo total do mês: USD ${TOTAL}"
echo ""
# Custo por serviço AWS
echo "📊 Custo por serviço:"
aws ce get-cost-and-usage \
--time-period Start=${MES_INICIO},End=${MES_FIM} \
--granularity MONTHLY \
--metrics BlendedCost \
--group-by Type=DIMENSION,Key=SERVICE \
--query 'ResultsByTime[0].Groups[*].{
Servico: Keys[0],
Custo: Metrics.BlendedCost.Amount
}' \
--output json \
| jq 'sort_by(.Custo | tonumber) | reverse | .[:10][] |
" \(.Servico): USD \(.Custo | tonumber | . * 100 | round / 100)"' \
-r
echo ""
# Custo por equipe (via tag Team)
echo "👥 Custo por equipe:"
aws ce get-cost-and-usage \
--time-period Start=${MES_INICIO},End=${MES_FIM} \
--granularity MONTHLY \
--metrics BlendedCost \
--group-by Type=TAG,Key=Team \
--query 'ResultsByTime[0].Groups[*].{
Equipe: Keys[0],
Custo: Metrics.BlendedCost.Amount
}' \
--output json \
| jq '.[] | " \(.Equipe): USD \(.Custo | tonumber | . * 100 | round / 100)"' \
-r
echo ""
# Anomalias de custo detectadas
echo "⚠️ Anomalias detectadas:"
aws ce get-anomalies \
--date-interval StartDate=${MES_INICIO},EndDate=${MES_FIM} \
--query 'Anomalies[*].{
Servico: AnomalyScore.CurrentScore,
ImpactoUSD: Impact.TotalImpact,
Inicio: AnomalyStartDate
}' \
--output json \
| jq '.[] | select(.ImpactoUSD > 100) |
" Impacto USD \(.ImpactoUSD | round) desde \(.Inicio)"' \
-r
Alertas de Orçamento
# budget.tf
resource "aws_budgets_budget" "mensal" {
name = "${var.project_name}-${var.environment}-mensal"
budget_type = "COST"
limit_amount = var.budget_mensal_usd
limit_unit = "USD"
time_unit = "MONTHLY"
time_period_start = "2025-01-01_00:00"
# Alertas em 70%, 90% e 100% do orçamento
notification {
comparison_operator = "GREATER_THAN"
threshold = 70
threshold_type = "PERCENTAGE"
notification_type = "FORECASTED"
subscriber_email_addresses = [var.finops_email]
}
notification {
comparison_operator = "GREATER_THAN"
threshold = 90
threshold_type = "PERCENTAGE"
notification_type = "FORECASTED"
subscriber_email_addresses = [var.finops_email]
subscriber_sns_topic_arns = [aws_sns_topic.alertas_custo.arn]
}
notification {
comparison_operator = "GREATER_THAN"
threshold = 100
threshold_type = "PERCENTAGE"
notification_type = "ACTUAL"
subscriber_email_addresses = [var.finops_email]
subscriber_sns_topic_arns = [aws_sns_topic.alertas_custo.arn]
}
cost_filters = {
TagKeyValue = ["user:Environment$${var.environment}"]
}
}
# Detecção automática de anomalias de custo
resource "aws_ce_anomaly_monitor" "servicos" {
name = "${var.project_name}-monitor-anomalias"
monitor_type = "DIMENSIONAL"
monitor_dimension = "SERVICE"
}
resource "aws_ce_anomaly_subscription" "alertas" {
name = "${var.project_name}-alertas-anomalias"
frequency = "DAILY"
monitor_arn_list = [aws_ce_anomaly_monitor.servicos.arn]
subscriber {
type = "SNS"
address = aws_sns_topic.alertas_custo.arn
}
# Alerta apenas se o impacto for maior que USD 50
threshold_expression {
dimension {
key = "ANOMALY_TOTAL_IMPACT_ABSOLUTE"
values = ["50"]
match_options = ["GREATER_THAN_OR_EQUAL"]
}
}
}
Otimização de Compute: EC2, ECS e Lambda
Savings Plans e Reserved Instances
Para cargas de trabalho previsíveis — servidores de aplicação que rodam 24/7 — o uso de On-Demand é o tipo mais caro de compute. Duas alternativas oferecem descontos significativos:
Compute Savings Plans — compromisso de uso de uma quantidade de compute (em USD/hora) por 1 ou 3 anos, com desconto de até 66% sobre On-Demand. Aplicam-se automaticamente a EC2, Fargate e Lambda.
Reserved Instances — reserva de instâncias específicas com desconto de até 72% para comprometimento de 3 anos com pagamento antecipado.
# Identifica instâncias candidatas a Savings Plans
# Instâncias que rodam > 80% do tempo no último mês são boas candidatas
aws ce get-reservation-coverage \
--time-period Start=$(date -d "30 days ago" +%Y-%m-%d),End=$(date +%Y-%m-%d) \
--granularity MONTHLY \
--group-by Type=DIMENSION,Key=INSTANCE_TYPE \
--query 'CoveragesByTime[0].Groups[*].{
Tipo: Keys[0],
Cobertura: Coverage.CoverageHours.CoverageHoursPercentage,
OnDemandCusto: Coverage.CoverageHours.OnDemandHours
}' \
--output table
# Recomendações de Savings Plans da AWS
aws ce get-savings-plans-purchase-recommendation \
--savings-plans-type COMPUTE_SP \
--term-in-years ONE_YEAR \
--payment-option NO_UPFRONT \
--lookback-period-in-days THIRTY_DAYS \
--query 'SavingsPlansPurchaseRecommendation.{
PoupancaMensalEstimada: SavingsPlansDetails[0].EstimatedMonthlySavingsAmount,
CompromissoHorario: SavingsPlansDetails[0].HourlyCommitmentToPurchase
}'
Instâncias Spot para Cargas de Trabalho Tolerantes a Interrupção
Instâncias Spot oferecem desconto de até 90% sobre On-Demand. São adequadas para cargas que toleram interrupção — jobs de CI/CD, processamento em batch, workers de fila:
# spot-workers.tf
# Auto Scaling Group com mix de On-Demand e Spot
resource "aws_autoscaling_group" "workers" {
name = "${var.project_name}-${var.environment}-workers"
vpc_zone_identifier = module.vpc.ids_subnets_privadas
min_size = 1
max_size = 20
desired_capacity = 2
mixed_instances_policy {
instances_distribution {
# 20% On-Demand para baseline, 80% Spot para escala
on_demand_base_capacity = 1
on_demand_percentage_above_base_capacity = 20
spot_allocation_strategy = "price-capacity-optimized"
}
launch_template {
launch_template_specification {
launch_template_id = aws_launch_template.worker.id
version = "$Latest"
}
# Múltiplos tipos de instância aumentam disponibilidade de Spot
override {
instance_type = "m6i.large"
}
override {
instance_type = "m6a.large"
}
override {
instance_type = "m5.large"
}
override {
instance_type = "m5a.large"
}
override {
instance_type = "m7i.large"
}
}
}
tag {
key = "Name"
value = "${var.project_name}-${var.environment}-worker"
propagate_at_launch = true
}
}
# Handler de interrupção Spot — drena o worker antes da instância ser terminada
# A AWS notifica 2 minutos antes de interromper uma instância Spot
resource "aws_cloudwatch_event_rule" "spot_interruption" {
name = "spot-interruption-${var.environment}"
description = "Detecta notificações de interrupção de instâncias Spot"
event_pattern = jsonencode({
source = ["aws.ec2"]
detail-type = ["EC2 Spot Instance Interruption Warning"]
})
}
resource "aws_cloudwatch_event_target" "spot_interruption_lambda" {
rule = aws_cloudwatch_event_rule.spot_interruption.name
target_id = "DrenaWorker"
arn = aws_lambda_function.drena_worker.arn
}
Rightsizing: Dimensionamento Correto de Instâncias
Um dos maiores desperdícios em cloud é o over-provisioning — instâncias dimensionadas para picos que raramente ocorrem. O AWS Compute Optimizer analisa métricas históricas e recomenda o tipo de instância ideal:
#!/bin/bash
# scripts/rightsizing-report.sh
# Gera relatório de recomendações de rightsizing
echo "=== Recomendações de Rightsizing — AWS Compute Optimizer ==="
echo ""
# Recomendações para instâncias EC2
echo "📦 Instâncias EC2 com recomendação de downsizing:"
aws compute-optimizer get-ec2-instance-recommendations \
--filters Name=Finding,Values=Overprovisioned \
--query 'instanceRecommendations[*].{
Instancia: instanceName,
AtualTipo: currentInstanceType,
RecomendadoTipo: recommendationOptions[0].instanceType,
PoupancaMensal: recommendationOptions[0].estimatedMonthlySavings.value,
Moeda: recommendationOptions[0].estimatedMonthlySavings.currency,
UsoMaxCPU: utilizationMetrics[?name==`CPU`].value | [0]
}' \
--output json \
| jq '.[] | "\(.Instancia): \(.AtualTipo) → \(.RecomendadoTipo) | CPU max: \(.UsoMaxCPU)% | Economia: \(.Moeda) \(.PoupancaMensal)/mês"' \
-r
echo ""
# Recomendações para funções Lambda
echo "λ Funções Lambda sub ou sobredimensionadas:"
aws compute-optimizer get-lambda-function-recommendations \
--filters Name=Finding,Values=Overprovisioned \
--query 'lambdaFunctionRecommendations[*].{
Funcao: functionArn,
MemoriaAtual: memorySizeRecommendationOptions[0].memorySize,
MemoriaRecomendada: memorySizeRecommendationOptions[0].memorySize,
Poupanca: memorySizeRecommendationOptions[0].projectedUtilizationMetrics[0].value
}' \
--output json \
| jq '.[] | "\(.Funcao | split(":") | .[-1]): → \(.MemoriaRecomendada)MB"' \
-r
Otimização de Banco de Dados
Identificando Queries Lentas com Performance Insights
#!/usr/bin/env python3
# scripts/analise-queries-lentas.py
# Identifica as queries mais custosas no RDS via Performance Insights
import boto3
import json
from datetime import datetime, timedelta, timezone
pi = boto3.client('pi', region_name='us-east-1')
rds = boto3.client('rds', region_name='us-east-1')
INSTANCIA = 'minha-api-production-db'
PERIODO_HORAS = 24
# Obtém o ResourceIdentifier da instância RDS
response = rds.describe_db_instances(
DBInstanceIdentifier=INSTANCIA
)
resource_id = response['DBInstances'][0]['DbiResourceId']
fim = datetime.now(timezone.utc)
inicio = fim - timedelta(hours=PERIODO_HORAS)
# Busca as top 10 queries por tempo total de execução
response = pi.get_resource_metrics(
ServiceType='RDS',
Identifier=resource_id,
StartTime=inicio,
EndTime=fim,
PeriodInSeconds=3600,
MetricQueries=[
{
'Metric': 'db.load.avg',
'GroupBy': {
'Group': 'db.sql',
'Dimensions': ['db.sql.statement'],
'Limit': 10,
}
}
]
)
print(f"=== Top 10 Queries por Carga — Últimas {PERIODO_HORAS}h ===\n")
for i, key in enumerate(
response['MetricList'][0].get('Keys', []), 1
):
statement = key['Dimensions'].get('db.sql.statement', 'N/A')
# Trunca queries longas para exibição
if len(statement) > 120:
statement = statement[:120] + '...'
# Calcula a carga média
valores = [
p['Value'] for p in key.get('DataPoints', [])
if 'Value' in p
]
carga_media = sum(valores) / len(valores) if valores else 0
print(f"{i}. Carga média: {carga_media:.2f} AAS")
print(f" Query: {statement}")
print()
print("\n💡 Queries com carga > 1.0 AAS merecem análise de índices.")
Otimizando Leituras com Connection Pooling
// src/database/pool.js — configuração otimizada do pool de conexões
const { Pool } = require('pg');
const pool = new Pool({
connectionString: process.env.DATABASE_URL,
ssl: { rejectUnauthorized: true },
// Dimensionamento do pool baseado na instância RDS
// Regra geral: min(max_connections / num_pods, 10)
max: parseInt(process.env.DB_POOL_MAX || '10'),
min: parseInt(process.env.DB_POOL_MIN || '2'),
// Tempo de idle antes de fechar conexão extra
idleTimeoutMillis: 30000,
// Timeout para obter conexão do pool
connectionTimeoutMillis: 5000,
// Timeout para queries individuais
statement_timeout: 30000, // 30 segundos
query_timeout: 30000,
// Keepalive para conexões de longa duração
keepAlive: true,
keepAliveInitialDelayMillis: 10000,
});
// Métricas do pool expostas para o Prometheus
const { Gauge } = require('prom-client');
const poolTotal = new Gauge({
name: 'db_pool_total_connections',
help: 'Total de conexões no pool',
});
const poolIdle = new Gauge({
name: 'db_pool_idle_connections',
help: 'Conexões ociosas no pool',
});
const poolWaiting = new Gauge({
name: 'db_pool_waiting_clients',
help: 'Clientes aguardando conexão disponível',
});
setInterval(() => {
poolTotal.set(pool.totalCount);
poolIdle.set(pool.idleCount);
poolWaiting.set(pool.waitingCount);
}, 5000);
pool.on('error', (err) => {
console.error(JSON.stringify({
level: 'error',
msg: 'Erro inesperado no pool de conexões',
err: err.message,
}));
});
module.exports = pool;
Otimização de Storage e Transferência de Dados
S3 Intelligent-Tiering e Lifecycle Policies
O storage é frequentemente o item de custo mais negligenciado — buckets crescem indefinidamente sem políticas de ciclo de vida:
# s3-lifecycle.tf
resource "aws_s3_bucket_lifecycle_configuration" "dados" {
bucket = aws_s3_bucket.dados.id
# Logs de aplicação — transitam para Glacier e são deletados
rule {
id = "lifecycle-logs-aplicacao"
status = "Enabled"
filter {
prefix = "logs/"
}
transition {
days = 30
storage_class = "STANDARD_IA" # Acesso infrequente após 30 dias
}
transition {
days = 90
storage_class = "GLACIER_IR" # Glacier Instant Retrieval após 90 dias
}
expiration {
days = 365 # Deleta após 1 ano
}
noncurrent_version_expiration {
noncurrent_days = 30 # Versões antigas deletadas em 30 dias
}
}
# Backups — retidos mais tempo, transitam para Glacier Deep Archive
rule {
id = "lifecycle-backups"
status = "Enabled"
filter {
prefix = "backups/"
}
transition {
days = 7
storage_class = "STANDARD_IA"
}
transition {
days = 30
storage_class = "GLACIER"
}
transition {
days = 90
storage_class = "DEEP_ARCHIVE" # USD 0.00099/GB — mais barato possível
}
expiration {
days = 2555 # 7 anos para compliance
}
}
# Assets de frontend — Intelligent-Tiering gerencia automaticamente
rule {
id = "lifecycle-assets-frontend"
status = "Enabled"
filter {
prefix = "frontend/"
}
transition {
days = 0
storage_class = "INTELLIGENT_TIERING"
}
}
}
# Configuração do Intelligent-Tiering para assets
resource "aws_s3_bucket_intelligent_tiering_configuration" "assets" {
bucket = aws_s3_bucket.dados.id
name = "assets-inteligente"
filter {
prefix = "frontend/"
}
tiering {
access_tier = "DEEP_ARCHIVE_ACCESS"
days = 180
}
tiering {
access_tier = "ARCHIVE_ACCESS"
days = 90
}
}
Reduzindo Custos de Transferência de Dados
Transferência de dados entre regiões AWS e para a internet é uma das fontes de custo mais surpreendentes. Estratégias para reduzir:
# vpc-endpoints.tf — acessa serviços AWS sem tráfego de internet
# Gateway Endpoint para S3 — gratuito, elimina tráfego para internet
resource "aws_vpc_endpoint" "s3" {
vpc_id = module.vpc.vpc_id
service_name = "com.amazonaws.${var.aws_region}.s3"
vpc_endpoint_type = "Gateway"
route_table_ids = concat(
module.vpc.ids_route_tables_privadas,
module.vpc.ids_route_tables_publicas
)
tags = merge(local.tags_comuns, {
Name = "${var.project_name}-${var.environment}-s3-endpoint"
})
}
# Interface Endpoint para Secrets Manager
# Evita tráfego de internet — cobra por hora + GB processado
resource "aws_vpc_endpoint" "secrets_manager" {
vpc_id = module.vpc.vpc_id
service_name = "com.amazonaws.${var.aws_region}.secretsmanager"
vpc_endpoint_type = "Interface"
private_dns_enabled = true
subnet_ids = module.vpc.ids_subnets_privadas
security_group_ids = [aws_security_group.vpc_endpoints.id]
tags = merge(local.tags_comuns, {
Name = "${var.project_name}-${var.environment}-secretsmanager-endpoint"
})
}
# Interface Endpoint para ECR — acelera pull de imagens em EKS/ECS
resource "aws_vpc_endpoint" "ecr_api" {
vpc_id = module.vpc.vpc_id
service_name = "com.amazonaws.${var.aws_region}.ecr.api"
vpc_endpoint_type = "Interface"
private_dns_enabled = true
subnet_ids = module.vpc.ids_subnets_privadas
security_group_ids = [aws_security_group.vpc_endpoints.id]
tags = local.tags_comuns
}
resource "aws_vpc_endpoint" "ecr_dkr" {
vpc_id = module.vpc.vpc_id
service_name = "com.amazonaws.${var.aws_region}.ecr.dkr"
vpc_endpoint_type = "Interface"
private_dns_enabled = true
subnet_ids = module.vpc.ids_subnets_privadas
security_group_ids = [aws_security_group.vpc_endpoints.id]
tags = local.tags_comuns
}
Dashboard FinOps no Grafana
Um dashboard dedicado a custos conecta métricas técnicas ao impacto financeiro:
{
"dashboard": {
"title": "FinOps — Custos e Eficiência",
"panels": [
{
"title": "Custo por Requisição Atendida",
"type": "stat",
"description": "USD gasto por cada 1000 requisições processadas — métrica de eficiência de custo",
"targets": [
{
"expr": "(aws_billing_estimated_charges / on()(sum(increase(http_requests_total[24h])) / 1000))",
"legendFormat": "USD / 1k req"
}
]
},
{
"title": "Custo Estimado do Dia (Projeção Mensal)",
"type": "timeseries",
"targets": [
{
"expr": "aws_billing_estimated_charges",
"legendFormat": "Custo acumulado USD"
}
]
},
{
"title": "Utilização de CPU dos Nós EKS",
"description": "Baixa utilização indica oportunidade de rightsizing ou consolidação",
"type": "gauge",
"targets": [
{
"expr": "avg(100 - (avg by(node) (irate(node_cpu_seconds_total{mode='idle'}[5m])) * 100))",
"legendFormat": "CPU média dos nós"
}
],
"thresholds": {
"steps": [
{"color": "red", "value": 0},
{"color": "yellow", "value": 40},
{"color": "green", "value": 60}
]
}
},
{
"title": "Memória Não Utilizada nos Nós",
"type": "timeseries",
"targets": [
{
"expr": "sum(node_memory_MemAvailable_bytes) / sum(node_memory_MemTotal_bytes) * 100",
"legendFormat": "% memória livre nos nós"
}
]
}
]
}
}
Custo do Pipeline de CI/CD
Os próprios pipelines de CI/CD têm um custo que cresce com a frequência de commits e o tamanho do time. Otimizações que reduzem o tempo de pipeline reduzem diretamente o custo de GitHub Actions ou do runner:
# Estratégias para pipelines mais eficientes
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
# Cache de node_modules — evita npm install em cada run
- uses: actions/cache@v4
id: cache-deps
with:
path: |
~/.npm
node_modules
key: ${{ runner.os }}-node-${{ hashFiles('package-lock.json') }}
restore-keys: |
${{ runner.os }}-node-
- name: Instala dependências (apenas se cache miss)
if: steps.cache-deps.outputs.cache-hit != 'true'
run: npm ci
# Cache de camadas Docker — evita rebuild de camadas inalteradas
- uses: actions/cache@v4
with:
path: /tmp/.buildx-cache
key: ${{ runner.os }}-buildx-${{ github.sha }}
restore-keys: |
${{ runner.os }}-buildx-
- uses: docker/setup-buildx-action@v3
- uses: docker/build-push-action@v5
with:
context: .
cache-from: type=local,src=/tmp/.buildx-cache
cache-to: type=local,dest=/tmp/.buildx-cache-new,mode=max
tags: ghcr.io/empresa/api:${{ github.sha }}
push: true
# Rotaciona o cache — evita crescimento indefinido
- name: Rotaciona cache do Buildx
run: |
rm -rf /tmp/.buildx-cache
mv /tmp/.buildx-cache-new /tmp/.buildx-cache
Relatório Semanal de FinOps Automatizado
# .github/workflows/relatorio-finops-semanal.yml
name: Relatório Semanal de FinOps
on:
schedule:
- cron: '0 8 * * MON' # Segunda-feira às 8h
workflow_dispatch:
jobs:
relatorio:
runs-on: ubuntu-latest
permissions:
id-token: write
contents: read
steps:
- uses: actions/checkout@v4
- uses: aws-actions/configure-aws-credentials@v4
with:
role-to-assume: ${{ secrets.AWS_FINOPS_ROLE_ARN }}
aws-region: us-east-1
- name: Gera relatório de custos
run: bash scripts/relatorio-custos.sh > relatorio.txt
- name: Gera recomendações de rightsizing
run: python3 scripts/analise-queries-lentas.py >> relatorio.txt
- name: Publica no Slack
uses: slackapi/slack-github-action@v1.26.0
with:
channel-id: ${{ vars.SLACK_FINOPS_CHANNEL }}
payload: |
{
"blocks": [
{
"type": "header",
"text": {
"type": "plain_text",
"text": "📊 Relatório Semanal de FinOps"
}
},
{
"type": "section",
"text": {
"type": "mrkdwn",
"text": "```${{ steps.relatorio.outputs.summary }}```"
}
},
{
"type": "actions",
"elements": [
{
"type": "button",
"text": {"type": "plain_text", "text": "Ver no AWS Cost Explorer"},
"url": "https://console.aws.amazon.com/cost-management/home"
}
]
}
]
}
env:
SLACK_BOT_TOKEN: ${{ secrets.SLACK_BOT_TOKEN }}
O Que Vem a Seguir
O próximo artigo cobre Resiliência e Chaos Engineering — como construir sistemas que falham graciosamente, como usar o AWS Fault Injection Simulator para testar a resiliência em produção e como implementar os padrões de resiliência fundamentais: circuit breaker, retry com backoff exponencial e bulkhead.
Referências para Aprofundamento
FinOps e otimização de custos
- FinOps Foundation — finops.org — Definição oficial do FinOps, incluindo o modelo de maturidade e as práticas recomendadas para times de engenharia e finanças.
- AWS Cost Optimization Hub — docs.aws.amazon.com — Centro de otimização de custos da AWS com recomendações consolidadas de Savings Plans, rightsizing e recursos ociosos.
- AWS Compute Optimizer — docs.aws.amazon.com — Documentação do Compute Optimizer com guia de interpretação das recomendações e integração com AWS Organizations.
Exercícios
Exercício 1
Analise a regra lifecycle-backups do s3-lifecycle.tf: transição para STANDARD_IA aos 7 dias, para GLACIER aos 30 e para DEEP_ARCHIVE aos 90. A intenção é clara — mover o dado para camadas mais baratas o quanto antes. Por que essa configuração não economiza o que aparenta?
Ver resposta
✓ Resposta: Porque cada classe de armazenamento tem uma duração mínima faturável, e a regra viola duas delas. Sair antes do mínimo não economiza: a AWS cobra o período inteiro de qualquer forma.
| classe | mínimo | tempo na regra |
|---|---|---|
STANDARD_IA | 30 dias | 7 → 30 = 23 dias ✗ |
GLACIER | 90 dias | 30 → 90 = 60 dias ✗ |
DEEP_ARCHIVE | 180 dias | 90 → 2555 ✓ |
Ou seja: o objeto passa 23 dias no IA e é cobrado por 30; passa 60 dias no Glacier e é cobrado por 90. E cada transição tem custo próprio por objeto — com muitos arquivos pequenos, o custo das requisições de transição pode superar a economia de armazenamento. Objetos menores que 128 KB agravam: eles nem são elegíveis para transição automática, e no IA são cobrados como se tivessem 128 KB.
A regra corrigida respeita os patamares, mantendo a mesma intenção:
transition {
days = 30 # cumpre o mínimo do IA antes de sair
storage_class = "STANDARD_IA"
}
transition {
days = 120 # 90 dias completos no Glacier
storage_class = "GLACIER"
}
transition {
days = 210 # 90 dias completos antes do Deep Archive
storage_class = "DEEP_ARCHIVE"
}
Repare que a regra lifecycle-logs-aplicacao, logo acima, está correta: 30 dias em Standard, 60 no IA (mínimo 30 ✓) e 275 no Glacier IR (mínimo 90 ✓). A diferença entre as duas é exatamente o tipo de detalhe que só aparece na fatura, dois meses depois — e é por isso que o lifecycle-assets-frontend delega ao Intelligent-Tiering, que move os objetos sozinho conforme o padrão real de acesso, sem cobrança de recuperação e sem esse cálculo manual.
Exercício 2
A SCP ExigirEtiquetasObrigatorias pretende bloquear a criação de recursos sem a etiqueta Project. Um engenheiro executa aws ec2 run-instances sem nenhuma etiqueta e a instância é criada normalmente. Onde está o erro na condição?
Ver resposta
✓ Resposta: A política usa aws:ResourceTag/Project, que é a chave errada para este caso. aws:ResourceTag avalia as etiquetas de um recurso que já existe — e no momento do RunInstances a instância ainda não existe, então não há etiqueta a inspecionar. A chave que examina as etiquetas enviadas na requisição é aws:RequestTag/Project.
Há um segundo problema no mesmo bloco. As duas chaves estão dentro do mesmo operador Null, e chaves no mesmo operador se combinam com E. A condição só seria verdadeira se aws:RequestedRegion fosse nula ("false" diz que ela não é nula — o que é sempre verdade) e a tag fosse nula. A presença dessa chave de região ali não tem função; ela apenas embaralha a leitura da política.
A forma correta de exigir a etiqueta na criação:
{
"Sid": "ExigirEtiquetaProject",
"Effect": "Deny",
"Action": ["ec2:RunInstances", "rds:CreateDBInstance"],
"Resource": "*",
"Condition": {
"Null": { "aws:RequestTag/Project": "true" }
}
}
Duas notas sobre SCPs de etiqueta. Elas não concedem permissão — apenas limitam o teto do que as políticas de IAM podem permitir dentro da organização; e como o efeito é Deny, um erro de escrita costuma falhar em aberto, exatamente como aqui: nada quebra, e a governança simplesmente não acontece. Vale também lembrar que o ec2:RunInstances cria vários tipos de recurso (instância, volume, interface de rede), então uma política completa precisa alcançar também o ec2:CreateTags da mesma requisição.
O timestamp() no CreatedAt das tags_comuns merece atenção pelo mesmo motivo de disciplina: essa função devolve um valor novo a cada execução, então todo terraform plan acusa mudança em todos os recursos que a usam.
Exercício 3
No spot-workers.tf, o Auto Scaling Group tem on_demand_base_capacity = 1 e on_demand_percentage_above_base_capacity = 20. Quando o grupo escala para 11 instâncias, quantas serão On-Demand e quantas Spot? E por que o bloco declara cinco tipos de instância diferentes em override?
Ver resposta
✓ Resposta: 3 On-Demand e 8 Spot. O cálculo tem duas partes: a on_demand_base_capacity reserva 1 instância On-Demand como piso garantido, e a porcentagem se aplica apenas ao que passa desse piso — 20% das 10 restantes = 2. Total: 1 + 2 = 3 On-Demand, e as outras 8 em Spot.
A base existe para garantir capacidade mínima que não pode evaporar: mesmo que o mercado Spot fique indisponível por completo, o serviço continua com aquela instância. É o desenho típico — baseline confiável em On-Demand, elasticidade barata em Spot.
Os cinco override respondem a como o Spot funciona: cada combinação de tipo de instância + zona de disponibilidade é um pool de capacidade separado, com preço e disponibilidade próprios. Pedir apenas m6i.large significa depender de um único pool por AZ; quando ele se esgota, o grupo simplesmente não escala. Cinco tipos equivalentes multiplicam os pools disponíveis, e a estratégia price-capacity-optimized escolhe entre eles ponderando preço e profundidade de capacidade — priorizando os pools com menor chance de interrupção, não apenas o mais barato do momento.
O aws_cloudwatch_event_rule de EC2 Spot Instance Interruption Warning fecha o desenho: a AWS avisa dois minutos antes de retomar a instância, e a Lambda usa essa janela para drenar o worker — parar de aceitar trabalho novo e concluir o que está em andamento. Sem esse handler, o desconto de até 90% é pago com trabalho perdido no meio da execução.
Exercício 4
No rightsizing-report.sh, a seção de Lambda imprime a memória atual e a recomendada para cada função. O relatório sai com os dois valores sempre idênticos, para todas as funções. Por quê?
Ver resposta
✓ Resposta: Os dois campos leem exatamente a mesma coisa:
MemoriaAtual: memorySizeRecommendationOptions[0].memorySize,
MemoriaRecomendada: memorySizeRecommendationOptions[0].memorySize,
Ambos apontam para a memória recomendada. O valor atual da função está em outro campo da resposta — currentMemorySize. Como está, o relatório é incapaz de mostrar economia alguma: ele compara a recomendação com ela mesma.
MemoriaAtual: currentMemorySize,
MemoriaRecomendada: memorySizeRecommendationOptions[0].memorySize,
Há um segundo desalinhamento na mesma seção: o título anuncia funções "sub ou sobredimensionadas", mas o filtro é Values=Overprovisioned — só as sobredimensionadas aparecem. E o campo rotulado Poupanca lê projectedUtilizationMetrics[0].value, que é uma projeção de duração de execução, não um valor em dólares.
Vale notar por que subdimensionada importa tanto quanto o contrário em Lambda, e é justamente o que o filtro esconde: a memória alocada determina também a fatia de CPU que a função recebe. Uma função com pouca memória roda mais devagar, e como a cobrança é por GB-segundo, dobrar a memória pode cortar o tempo de execução pela metade e sair pelo mesmo preço — com metade da latência. É o caso em que aumentar o recurso não aumenta o custo, algo que não tem equivalente em EC2.
Exercício 5
O painel "Custo por Requisição Atendida" define a métrica de eficiência assim:
(aws_billing_estimated_charges / on()(sum(increase(http_requests_total[24h])) / 1000))
Um time com tráfego e infraestrutura perfeitamente estáveis observa esse número subindo todo dia, e despencando no primeiro dia de cada mês. O que a fórmula está realmente medindo?
Ver resposta
✓ Resposta: A fórmula divide grandezas de janelas de tempo diferentes. O aws_billing_estimated_charges é o custo acumulado desde o início do mês — ele só cresce, e zera na virada. O denominador conta as requisições das últimas 24 horas, que permanecem estáveis.
O resultado é acumulado-do-mês dividido por tráfego-de-um-dia: no dia 10 o valor é dez vezes o do dia 1, sem que nada tenha mudado na eficiência. O painel não mede custo por requisição — mede, com passos extras, quantos dias se passaram desde o dia 1º. O mergulho na virada do mês é o contador zerando, e não uma melhoria.
Para que a métrica signifique o que promete, os dois lados precisam cobrir o mesmo intervalo — usando o incremento diário do custo:
increase(aws_billing_estimated_charges[24h])
/ on()
(sum(increase(http_requests_total[24h])) / 1000)
Duas ressalvas de ordem prática. O aws_billing_estimated_charges vem do CloudWatch com atualização de poucas vezes por dia e com atraso de várias horas — é uma estimativa grosseira, adequada para tendência, nunca para conciliação financeira, que continua sendo trabalho do Cost Explorer com os dados de faturamento. E uma métrica de custo por requisição só é comparável ao longo do tempo se o denominador for estável no significado: se http_requests_total passar a contar também health checks e chamadas internas de service mesh, o número melhora sem que nada tenha ficado mais eficiente.
É o mesmo risco que o painel de CPU logo abaixo evita ao trazer a descrição explícita de que baixa utilização indica oportunidade de rightsizing: em um dashboard de FinOps, o que o número quer dizer precisa estar escrito ao lado dele — quem lê está decidindo onde cortar gasto.