Contexto
Eu trabalho em uma empresa do setor de exportação. Todos os dias, o time financeiro precisa da cotação do dólar (PTAX) atualizada no ERP para precificação de contratos. A fonte oficial é a API pública do Banco Central do Brasil.
O pipeline antigo fazia isso: um arquivo .ktr do Pentaho Data Integration, agendado no Jenkins rodando em um EC2, consultava a API do BCB, transformava o retorno e gravava no Firebird (o banco legado do ERP). Funcionou durante anos — até que um dia parou silenciosamente. Ninguém foi notificado. O financeiro só percebeu quando os preços começaram a sair errados.
Esse foi o gatilho para eu reescrever tudo do zero em uma arquitetura serverless.
Os gargalos
Mapeei os problemas do pipeline antigo lado a lado com o que eu queria na nova arquitetura:
Antes (Jenkins + Pentaho)
- Senha hardcoded em XML (plain text no .ktr)
- Sem alertas de falha — erros silenciosos
- Janela fixa de 5 dias úteis (hardcoded)
- EC2 rodando 24/7 para um job de 5 segundos/dia
- Race condition na geração de PK
- Timeout de 50 minutos configurado no Jenkins
- 500+ linhas de XML no arquivo .ktr
Depois (Lambda + EventBridge)
- Secrets Manager + KMS (credenciais criptografadas)
- SNS envia e-mail em qualquer erro ou sucesso
- Janela dinâmica via
MAX(DATA)no banco - Serverless — Lambda roda só quando necessário
- PK sequencial calculada no código
- Timeout de 60 segundos (execução real: ~5s)
- ~100 linhas de Python legível
Arquitetura
O fluxo completo da nova solução:
Serviços utilizados
Cada serviço e por que ele está na arquitetura:
| Serviço | Função | Por que este e não outro |
|---|---|---|
| AWS Lambda | Executa o código Python que consulta a API do BCB e grava no Firebird | Workload de ~5s/dia. Pagar EC2 24/7 para isso é desperdício. Lambda cobra por milissegundo. |
| EventBridge Scheduler | Agenda a execução da Lambda (seg-sex, 18:05 BRT) | Suporta timezone nativo (America/Sao_Paulo). CloudWatch Events não suporta. |
| Secrets Manager | Armazena credenciais do Firebird criptografadas com KMS | Rotação automática, audit trail, zero plain text. |
| CloudWatch | Logs estruturados e métricas de cada execução | Integração nativa com Lambda. Sem setup extra. |
| SNS | Envia e-mail com resultado de cada execução (sucesso ou erro) | Simples, barato, integra direto com Lambda via SDK. |
| IAM | Roles e policies de least privilege para a Lambda | A Lambda só acessa os recursos que precisa. Nada mais. |
Implementação passo a passo
Engenharia reversa do pipeline Pentaho
O arquivo .ktr original tinha mais de 500 linhas de XML. Abri no Spoon (editor visual do Pentaho) e mapeei cada step. Na prática, o pipeline fazia apenas 4 operações úteis: (1) conectar no Firebird, (2) consultar a API do BCB, (3) transformar o JSON, (4) inserir no banco. O resto era boilerplate XML, tratamento de erro inútil (steps Dummy) e configurações de conexão duplicadas.
Reescrita em Python (~100 linhas)
Reescrevi as 4 operações em Python puro. A diferença principal: em vez de uma janela fixa de 5 dias, eu consulto MAX(DATA) no banco para saber a última cotação gravada e busco apenas o que falta. Isso torna o pipeline idempotente — posso rodar quantas vezes quiser sem duplicar dados.
lambda_handler.py (clique para expandir)
import json
import os
import logging
from datetime import datetime, timedelta
import firebirdsql
import boto3
import urllib.request
logger = logging.getLogger()
logger.setLevel(logging.INFO)
sns = boto3.client("sns")
sm = boto3.client("secretsmanager")
SNS_TOPIC_ARN = os.environ["SNS_TOPIC_ARN"]
SECRET_NAME = os.environ["SECRET_NAME"]
FB_DATABASE = os.environ["FB_DATABASE"]
BCB_URL = (
"https://olinda.bcb.gov.br/olinda/servico/PTAX/versao/v1/"
"odata/CotacaoDolarPeriodo(dataInicial=@di,dataFinalCotacao=@df)"
"?@di='{start}'&@df='{end}'"
"&$format=json&$orderby=dataHoraCotacao%20asc"
)
def lambda_handler(event, context):
"""Pipeline PTAX: 7 passos sequenciais."""
# 1. Buscar credenciais no Secrets Manager
secret = json.loads(
sm.get_secret_value(SecretId=SECRET_NAME)["SecretString"]
)
logger.info("Credenciais obtidas do Secrets Manager")
# 2. Conectar no Firebird
conn = firebirdsql.connect(
host=secret["host"],
port=int(secret["port"]),
database=FB_DATABASE,
user=secret["user"],
password=secret["password"],
charset="UTF8",
)
cur = conn.cursor()
logger.info("Conectado ao Firebird")
# 3. Obter estado atual (ultima data + proximo ID)
cur.execute("SELECT MAX(DATA) FROM COTACAO_DOLAR")
last_date = cur.fetchone()[0]
cur.execute("SELECT MAX(ID) FROM COTACAO_DOLAR")
max_id = cur.fetchone()[0] or 0
start = (last_date + timedelta(days=1)).strftime("%m-%d-%Y") if last_date else "01-01-2024"
end = datetime.now().strftime("%m-%d-%Y")
logger.info(f"Janela dinamica: {start} ate {end}")
# 4. Consultar API PTAX do BCB
url = BCB_URL.format(start=start, end=end)
with urllib.request.urlopen(url, timeout=15) as resp:
data = json.loads(resp.read().decode())
rows = data.get("value", [])
logger.info(f"API retornou {len(rows)} cotacoes")
# 5. Inserir novas cotacoes
inserted = 0
for row in rows:
max_id += 1
dt = datetime.strptime(
row["dataHoraCotacao"][:10], "%Y-%m-%d"
).date()
cur.execute(
"INSERT INTO COTACAO_DOLAR (ID, DATA, COMPRA, VENDA) VALUES (?, ?, ?, ?)",
(max_id, dt, row["cotacaoCompra"], row["cotacaoVenda"]),
)
inserted += 1
conn.commit()
cur.close()
conn.close()
logger.info(f"{inserted} cotacoes inseridas")
# 6. Notificar via SNS
msg = f"PTAX pipeline OK: {inserted} cotacoes inseridas ({start} a {end})"
sns.publish(TopicArn=SNS_TOPIC_ARN, Subject="PTAX Pipeline", Message=msg)
# 7. Retornar resultado
return {
"statusCode": 200,
"body": json.dumps({
"inserted": inserted,
"window": {"start": start, "end": end},
}),
}
Testes locais com pytest
Escrevi 9 testes unitários cobrindo os cenários críticos: API retornando vazio, API retornando dados válidos, banco sem registros anteriores (cold start), tratamento de erros de conexão, e validação do payload SNS. Todos rodando com mocks — sem depender de infra real.
Mapear infra existente
Antes de criar qualquer recurso, inspecionei a conta AWS. Descobri que já existia um Security Group configurado para acesso ao Firebird (porta 3050) na VPC corporativa. Em vez de criar um novo, referenciei o existente no Terraform com data.aws_security_group. Isso evitou duplicação e conflito de regras.
Terraform — Infrastructure as Code
Toda a infra está em Terraform: Lambda, EventBridge Scheduler, IAM Role, Secrets Manager, SNS Topic e CloudWatch Log Group. O deploy é reproduzível e auditável.
main.tf (trechos principais) (clique para expandir)
# ── Lambda Function ──────────────────────────────────────
resource "aws_lambda_function" "ptax" {
function_name = "bcb-ptax-ingestion"
runtime = "python3.12"
handler = "lambda_handler.lambda_handler"
architectures = ["arm64"] # Graviton — 20% mais barato
timeout = 60
memory_size = 128
filename = data.archive_file.lambda_zip.output_path
source_code_hash = data.archive_file.lambda_zip.output_base64sha256
role = aws_iam_role.lambda_role.arn
vpc_config {
subnet_ids = var.private_subnet_ids
security_group_ids = [data.aws_security_group.firebird.id]
}
environment {
variables = {
SECRET_NAME = aws_secretsmanager_secret.firebird_creds.name
FB_DATABASE = var.firebird_database_path
SNS_TOPIC_ARN = aws_sns_topic.ptax_alerts.arn
}
}
layers = [aws_lambda_layer_version.deps.arn]
depends_on = [aws_cloudwatch_log_group.ptax]
}
# ── EventBridge Scheduler ────────────────────────────────
resource "aws_scheduler_schedule" "ptax_daily" {
name = "ptax-daily-ingestion"
schedule_expression = "cron(5 18 ? * MON-FRI *)" # 18:05 BRT
schedule_expression_timezone = "America/Sao_Paulo"
flexible_time_window {
mode = "OFF"
}
target {
arn = aws_lambda_function.ptax.arn
role_arn = aws_iam_role.scheduler_role.arn
retry_policy {
maximum_retry_attempts = 2
}
}
}
Deploy e teste
Rodei terraform apply e invoquei a Lambda manualmente para validar. O primeiro resultado retornou 0 inserções porque o pipeline legado já tinha rodado naquele dia — exatamente o comportamento esperado (idempotência funcionando).
Resposta da primeira execução (clique para expandir)
{
"statusCode": 200,
"body": "{\"inserted\": 0, \"window\": {\"start\": \"03-31-2026\", \"end\": \"03-31-2026\"}}"
}
Decisões de arquitetura
O workload total é ~5 segundos por dia. Manter um container (ECS) ou uma instância (EC2) ligada 24/7 para isso seria desperdício de dinheiro e complexidade operacional. Lambda cobra por milissegundo de execução — o custo mensal é virtualmente zero.
Lambda com Graviton é 20% mais barato que x86 e tem performance equivalente (ou melhor) para workloads I/O bound como este. Como eu uso apenas bibliotecas pure Python (firebirdsql, urllib), não há dependência binária que quebre em ARM.
EventBridge Scheduler suporta timezone nativo (America/Sao_Paulo). Com CloudWatch Events, eu teria que calcular o offset UTC manualmente e lidar com horário de verão. Além disso, o Scheduler tem retry policy nativo (configurei 2 tentativas).
O Firebird roda em uma rede privada corporativa. A Lambda precisa estar na mesma VPC para acessar a porta 3050. O tradeoff é que Lambda em VPC precisa de NAT Gateway para acessar a internet (API do BCB) e o Secrets Manager — mas esse NAT já existia na infra.
Modelo de segurança
Três camadas de proteção:
As credenciais do Firebird ficam no Secrets Manager, criptografadas com uma chave KMS gerenciada pela AWS. A Lambda busca em runtime via SDK — nunca ficam em variável de ambiente, código-fonte ou repositório. Rotação automática disponível quando necessário.
A Lambda roda em subnets privadas da VPC corporativa. O Security Group permite apenas saída para a porta 3050 (Firebird) e HTTPS (443). Sem acesso SSH, sem portas abertas para a internet.
A IAM Role da Lambda tem permissões mínimas: secretsmanager:GetSecretValue apenas para o secret específico, logs:PutLogEvents para CloudWatch, sns:Publish para o tópico de alertas, e permissões de VPC networking. Nada mais.
Custo
| Componente | Custo estimado/mês |
|---|---|
| Lambda (22 invocações/mês, ~5s cada, 128 MB ARM64) | ~R$ 0,00 (free tier) |
| EventBridge Scheduler | ~R$ 0,00 (free tier) |
| Secrets Manager (1 secret, ~22 chamadas) | ~R$ 2,50 |
| CloudWatch Logs | ~R$ 0,10 |
| SNS (22 e-mails) | ~R$ 0,00 |
| NAT Gateway | Já existente (compartilhado) |
| Total | ~R$ 3,00/mês |
A infraestrutura anterior custava ~R$ 200/mês (EC2 t3.medium 24/7 + licença Jenkins + manutenção). A nova custa 98,5% menos.
O que aprendi
Eu quase criei um Security Group novo para a Lambda. Quando inspecionei a VPC, descobri que já existia um SG configurado com acesso ao Firebird. Referenciei com data.aws_security_group no Terraform e economizei tempo e conflitos de regras.
Durante o desenvolvimento, tentei testar a Lambda via aws lambda invoke no PowerShell. O retorno JSON vinha com encoding errado e aspas escapadas de forma diferente do bash. Perdi tempo até perceber que o problema não era a Lambda — era o shell. Passei a testar sempre via bash ou diretamente no console da AWS.
Quando coloquei a Lambda na VPC para acessar o Firebird, ela perdeu acesso à internet (API do BCB e Secrets Manager). Lambda em VPC roda em subnet privada — sem NAT Gateway, não sai para a internet. No meu caso, o NAT já existia, mas se não existisse seria um custo adicional (~$32/mês). Alternativa: VPC Endpoints para Secrets Manager.
Repositório
Código-fonte, Terraform e testes disponíveis em: github.com/cloudbymcn/bcb-ptax-ingestion