Objetivo da Aula
Ao concluir esta aula, você será capaz de:
Implementar detectar_recursos_ociosos() com critérios multi-tipo (EC2, RDS, EBS, ALB)
Implementar forecast_custos() com regressão linear simples
Analisar rightsizing: identificar recursos super-provisionados por CPU/memória
Usar Claude para gerar plano de ação FinOps priorizado
Completar os 2 TODOs do starter
Por que isso importa
FinOps (Financial Operations) é a disciplina de otimizar custos de cloud. Estudo da CNCF (2023) mostra que empresas desperdiçam em média 32% do gasto de cloud com recursos subutilizados ou super-provisionados.
Exemplos do lab: i-004 é um r5.2xlarge ($380/mês) com 3% de CPU e 8% de memória. Um r5.xlarge ($190/mês) é suficiente — economia de $190/mês com zero impacto funcional. db-002 é um db.r5.xlarge ($370/mês) com 2% de CPU — claramente desnecessário.
Conceitos Fundamentais
Inventário de Recursos Mock
RECURSOS = [
{'id': 'i-001', 'tipo': 'ec2', 'familia': 'm5.xlarge', 'cpu_avg': 8, 'mem_avg': 15, 'custo_mensal': 140},
{'id': 'i-002', 'tipo': 'ec2', 'familia': 'm5.xlarge', 'cpu_avg': 5, 'mem_avg': 12, 'custo_mensal': 140},
{'id': 'i-004', 'tipo': 'ec2', 'familia': 'r5.2xlarge', 'cpu_avg': 3, 'mem_avg': 8, 'custo_mensal': 380},
{'id': 'db-002', 'tipo': 'rds', 'familia': 'db.r5.xlarge', 'cpu_avg': 2, 'mem_avg': 5, 'custo_mensal': 370},
{'id': 'ebs-001', 'tipo': 'ebs', 'familia': 'gp3', 'tamanho_gb': 500, 'uso_gb': 45, 'custo_mensal': 40},
{'id': 'lb-001', 'tipo': 'alb', 'requests_per_day': 50000, 'custo_mensal': 25},
]Cada tipo de recurso tem métricas diferentes: EC2/RDS têm cpu_avg e mem_avg, EBS tem uso_gb/tamanho_gb, ALB tem requests_per_day.
TODO 1: detectar_recursos_ociosos(recursos)
def detectar_recursos_ociosos(recursos: list[dict]) -> list[dict]:
ociosos = []
for rec in recursos:
motivo = None
if rec['tipo'] in ('ec2', 'rds'):
cpu = rec.get('cpu_avg', 100)
mem = rec.get('mem_avg', 100)
if cpu < 2 and mem < 10:
motivo = f'CPU {cpu}% e memória {mem}% abaixo do threshold por 7+ dias'
elif rec['tipo'] == 'ebs':
tamanho = rec.get('tamanho_gb', 1)
uso = rec.get('uso_gb', tamanho)
uso_pct = uso / tamanho * 100
if uso_pct < 10:
motivo = f'Disco apenas {uso_pct:.1f}% utilizado ({uso}GB de {tamanho}GB)'
elif rec['tipo'] == 'alb':
rpd = rec.get('requests_per_day', 999999)
if rpd < 100:
motivo = f'Apenas {rpd} requisições/dia — provavelmente não utilizado'
elif rec['tipo'] == 'elasticache':
cpu = rec.get('cpu_avg', 100)
if cpu < 1:
motivo = f'Cache com CPU {cpu}% — provavelmente sem hits'
if motivo:
ociosos.append({
**rec,
'motivo': motivo,
'acao_recomendada': 'Desligar ou deletar recurso após confirmação',
})
return sorted(ociosos, key=lambda r: r['custo_mensal'], reverse=True)Critérios por tipo: - EC2/RDS: cpu < 2% E mem < 10% - EBS: uso_gb / tamanho_gb < 10% - ALB: requests_per_day < 100 - ElastiCache: cpu < 1%
TODO 2: forecast_custos(historico_mensal, meses_futuros)
Regressão linear simples para projetar tendência:
def forecast_custos(historico_mensal: list[float], meses_futuros: int = 3) -> list[dict]:
n = len(historico_mensal)
if n < 2:
return []
# Regressão linear: y = a*x + b
xs = list(range(n))
x_medio = sum(xs) / n
y_medio = sum(historico_mensal) / n
numerador = sum((x - x_medio) * (y - y_medio) for x, y in zip(xs, historico_mensal))
denominador = sum((x - x_medio) ** 2 for x in xs)
a = numerador / denominador if denominador != 0 else 0 # inclinação
b = y_medio - a * x_medio # intercepto
# Gerar forecasts
forecasts = []
from datetime import datetime, timedelta
agora = datetime.utcnow()
for i in range(meses_futuros):
mes_idx = n + i
custo_estimado = a * mes_idx + b
# Intervalo de confiança: ±10% para simplicidade
margem = custo_estimado * 0.1
mes_alvo = (agora.replace(day=1) + timedelta(days=32 * (i + 1))).strftime('%Y-%m')
forecasts.append({
'mes': mes_alvo,
'custo_estimado': round(max(0, custo_estimado), 2),
'intervalo_confianca': (
round(max(0, custo_estimado - margem), 2),
round(custo_estimado + margem, 2),
),
'tendencia': 'crescimento' if a > 0 else 'queda' if a < 0 else 'estável',
})
return forecastsRegressão linear: encontra a linha y = a*x + b que melhor se ajusta ao histórico. a é a taxa de crescimento mensal.
Aprofundamento Técnico
analisar_rightsizing — Já Implementado
def analisar_rightsizing(recursos: list[dict]) -> list[dict]:
recomendacoes = []
for rec in recursos:
cpu = rec.get('cpu_avg', 50)
mem = rec.get('mem_avg', 50)
if cpu < 15 and mem < 20: # super-provisionado
alternativas = ALTERNATIVAS_RIGHTSIZING.get(rec.get('familia', ''), [])
if alternativas:
melhor = alternativas[0] # (família, custo, fração)
economia = rec['custo_mensal'] - melhor[1]
recomendacoes.append({
'recurso': rec['id'],
'familia_atual': rec['familia'],
'alternativa': melhor[0],
'economia_mensal': economia,
'confianca': 'alta' if cpu < 8 and mem < 15 else 'media',
})
return sorted(recomendacoes, key=lambda r: r['economia_mensal'], reverse=True)Critério: CPU < 15% E memória < 20% = super-provisionado. Alta confiança se ambos < 8% e < 15%.
Diferença: Ocioso vs Super-provisionado
Conceito | Critério | Ação |
Ocioso | CPU < 2%, uso mínimo | Desligar/deletar imediatamente |
Super-provisionado | CPU < 15%, memória < 20% | Fazer rightsizing (trocar instância) |
Otimizado | CPU 20-70% | Nenhuma ação necessária |
Recursos ociosos são candidatos a eliminação. Recursos super-provisionados são candidatos a redimensionamento.
Forecast com Prophet (Alternativa)
def forecast_custos_prophet(historico_mensal: list[float], meses_futuros: int = 3) -> list[dict]:
from prophet import Prophet
import pandas as pd
from datetime import datetime
# Criar datas para o histórico
datas = pd.date_range(end=datetime.today(), periods=len(historico_mensal), freq='MS')
df = pd.DataFrame({'ds': datas, 'y': historico_mensal})
model = Prophet(yearly_seasonality=False, weekly_seasonality=False)
model.fit(df)
future = model.make_future_dataframe(periods=meses_futuros, freq='MS')
forecast = model.predict(future)
resultados = []
for _, row in forecast.tail(meses_futuros).iterrows():
resultados.append({
'mes': row['ds'].strftime('%Y-%m'),
'custo_estimado': round(max(0, row['yhat']), 2),
'intervalo_confianca': (round(max(0, row['yhat_lower']), 2), round(row['yhat_upper'], 2)),
})
return resultadosExemplos Anotados
Exemplo 1: Pipeline Completo
# 1. Total atual
total = sum(r['custo_mensal'] for r in RECURSOS)
print(f'Custo total: ${total:,.2f}/mês')
# → Custo total: $1,460.00/mês
# 2. Rightsizing
recomendacoes = analisar_rightsizing(RECURSOS)
economia = sum(r['economia_mensal'] for r in recomendacoes)
print(f'Oportunidade de rightsizing: ${economia}/mês')
# → i-001 m5.xlarge → m5.large: -$70/mês [CPU: 8%, MEM: 15%]
# → i-004 r5.2xlarge → r5.xlarge: -$190/mês [CPU: 3%, MEM: 8%]
# 3. Ociosos
ociosos = detectar_recursos_ociosos(RECURSOS)
print(f'Recursos ociosos: {len(ociosos)}')
# → db-002 db.r5.xlarge $370/mês: CPU 2%, MEM 5%
# 4. Forecast
historico = [1200, 1280, 1320, 1460] # últimos 4 meses
projecoes = forecast_custos(historico, meses_futuros=3)
for p in projecoes:
print(f'{p["mes"]}: ${p["custo_estimado"]} (±{p["intervalo_confianca"][1] - p["custo_estimado"]:.0f})')Exemplo 2: Detecção de Ociosos com Todos os Tipos
recursos_mistos = [
{'id': 'i-zombi', 'tipo': 'ec2', 'cpu_avg': 0.5, 'mem_avg': 3, 'custo_mensal': 200},
{'id': 'ebs-enorme', 'tipo': 'ebs', 'tamanho_gb': 2000, 'uso_gb': 150, 'custo_mensal': 200},
{'id': 'lb-vazio', 'tipo': 'alb', 'requests_per_day': 5, 'custo_mensal': 25},
]
ociosos = detectar_recursos_ociosos(recursos_mistos)
for o in ociosos:
print(f"{o['id']} (${o['custo_mensal']}/mês): {o['motivo']}")
# → i-zombi: CPU 0.5% e memória 3% abaixo do threshold
# → ebs-enorme: Disco apenas 7.5% utilizado (150GB de 2000GB)
# → lb-vazio: Apenas 5 requisições/diaPadrões e Armadilhas
Padrões
Padrão 1: Ordenar por impacto financeiro
return sorted(ociosos, key=lambda r: r['custo_mensal'], reverse=True)O recurso mais caro ocioso aparece primeiro — foco no maior impacto.
Padrão 2: Critérios multi-dimensionais para ociosos
if cpu < 2 and mem < 10: # AMBOS devem ser baixos
# EC2 com CPU baixo mas memória alta pode estar cacheando algoUm critério só é perigoso: cpu < 2 pode ser um servidor de banco de dados em horário fora de pico.
Padrão 3: Intervalo de confiança no forecast
margem = custo_estimado * 0.1 # 10% de margem
intervalo = (custo_estimado - margem, custo_estimado + margem)Forecast sem intervalo de confiança dá falsa precisão.
Armadilhas
⚠️ Armadilha 1: Deletar recurso “ocioso” sem validar dependências
# Um EBS ocioso pode estar montado em backup ou snapshot
# Um EC2 com CPU baixo pode ser um servidor de análise periódica
# SEMPRE confirmar com o dono do recurso antes de deletar⚠️ Armadilha 2: Regressão linear com histórico sazonal
historico = [1000, 1500, 1000, 1500, 1000] # padrão bimestral
# Regressão linear: y ≈ constante → previsão incorreta
# Solução: usar Prophet com sazonalidade⚠️ Armadilha 3: ALTERNATIVAS_RIGHTSIZING sem cobertura de todas as famílias
ALTERNATIVAS_RIGHTSIZING = {
'm5.xlarge': [...],
'r5.2xlarge': [...],
# ← e se o recurso for c5.2xlarge? KeyError!
}
# Usar .get() com default vazio:
alternativas = ALTERNATIVAS_RIGHTSIZING.get(rec.get('familia', ''), [])Se não for realizar o laboratório, pule para o próximo capítulo.
Ponte para o Lab
2 TODOs no starter.py:
TODO 1 — detectar_recursos_ociosos(recursos):
def detectar_recursos_ociosos(recursos: list[dict]) -> list[dict]:
ociosos = []
for rec in recursos:
motivo = None
if rec['tipo'] in ('ec2', 'rds') and rec.get('cpu_avg', 100) < 2 and rec.get('mem_avg', 100) < 10:
motivo = f'CPU {rec["cpu_avg"]}%, MEM {rec["mem_avg"]}%'
elif rec['tipo'] == 'ebs':
pct = rec.get('uso_gb', rec.get('tamanho_gb', 1)) / rec.get('tamanho_gb', 1) * 100
if pct < 10: motivo = f'Disco {pct:.1f}% utilizado'
elif rec['tipo'] == 'alb' and rec.get('requests_per_day', 999) < 100:
motivo = f'{rec["requests_per_day"]} req/dia'
if motivo:
ociosos.append({**rec, 'motivo': motivo})
return sorted(ociosos, key=lambda r: r['custo_mensal'], reverse=True)TODO 2 — forecast_custos(historico_mensal, meses_futuros): Implemente a regressão linear mostrada na seção de TODOs acima.
Rode python starter.py e observe o relatório completo de FinOps.
Agora você está pronto para o lab.