mozak.tech Engenharia de IA Corporativa 30%

Parte I — Operações e Infraestrutura Potencializadas por IA

1.9 — Otimização Financeira de Nuvem com Análise Assistida por IA (FinOps)

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 forecasts

Regressã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 resultados

Exemplos 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/dia

Padrõ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 algo

Um 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', ''), [])
⚗ Laboratório prático — mozak.tech
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.