graph LR
A[Aplicativo Flutter] -->|HTTPS| B[API Gateway]
B -->|evento| C[Lambda Function]
C -->|SQL| D[(RDS PostgreSQL)]
C -->|mensagem| E[SQS Queue]
E -->|dispara| F[Lambda Processador]
F -->|SQL| D
C -->|logs| G[CloudWatch]
F -->|logs| G
Módulo 10 — Integração com Backend AWS
Você chegou a um módulo que representa uma virada de perspectiva em toda a disciplina. Nos módulos anteriores, você aprendeu a construir interfaces Flutter, a navegar entre telas, a criar formulários, a gerenciar estado com Provider e GetIt, a persistir dados localmente com SQLite e SharedPreferences e, no módulo anterior, a consumir APIs de servidores externos usando o pacote http. Você já sabe fazer perguntas para um backend — mas até agora esse backend sempre foi algo que alguém construiu para você. Neste módulo, você vai construir o seu próprio backend na nuvem, usando os serviços da Amazon Web Services.
O backend que você vai criar para o aplicativo de delivery é composto por quatro serviços da AWS que trabalham em conjunto: o API Gateway, que recebe as requisições HTTP vindas do Flutter; as Lambda Functions, que executam a lógica de negócio em resposta a cada requisição; o RDS PostgreSQL, que armazena todos os dados relacionais do sistema; e o SQS, que permite o processamento assíncrono de tarefas que não precisam de resposta imediata, como o enfileiramento de pedidos para a cozinha.
Este módulo também coincide com a Entrega Parcial do Projeto Integrador. Ao final desta semana, o seu grupo vai apresentar tudo o que foi desenvolvido até aqui — do ambiente configurado no Módulo 01 até a integração completa com o backend AWS. É um marco importante, e este material foi organizado para garantir que você chegue a essa entrega com total domínio do que está apresentando.
Estude cada seção com cuidado, execute os exemplos no console da AWS e no seu ambiente Flutter, e chegue à aula presencial pronto para integrar o backend ao Projeto Integrador do seu grupo.
Seção 1 — Computação Serverless: Uma Mudança de Paradigma
Antes de escrever uma única linha de código para o backend do delivery, você precisa entender o modelo de computação que tornará possível toda a arquitetura que estudará neste módulo. Esse modelo se chama serverless — e, apesar do nome sugestivo, não significa que não há servidores: eles existem, mas você simplesmente não precisa se preocupar com eles.
Para compreender o valor do modelo serverless, é útil fazer uma comparação com o modelo tradicional de servidores. Imagine que você precisasse hospedar o backend do aplicativo de delivery em uma abordagem convencional. Você alugaria uma máquina virtual na nuvem — por exemplo, uma instância EC2 na AWS — e instalaria nela um servidor de aplicação como o Gunicorn rodando uma aplicação Python, ou um servidor Express rodando Node.js. Essa máquina ficaria ligada vinte e quatro horas por dia, sete dias por semana, consumindo recursos e gerando custos independentemente de haver ou não requisições chegando. Se o tráfego de usuários triplicasse em um horário de pico — digamos, na hora do almoço, quando muitos pedidos são feitos — você precisaria ter dimensionado a máquina para suportar esse pico, o que significa que ela ficaria superdimensionada (e, portanto, cara) durante o restante do tempo.
O modelo serverless inverte completamente essa lógica. Em vez de manter um servidor sempre ligado aguardando requisições, você define unidades de lógica chamadas funções. Cada função é executada sob demanda, apenas quando uma requisição chega. O provedor de nuvem — no nosso caso, a AWS — gerencia toda a infraestrutura subjacente: provisionamento, escalabilidade, disponibilidade, patching do sistema operacional e reaproveitamento de recursos. Você paga apenas pelo tempo de execução de cada função e pelo número de invocações — não pelo tempo em que o servidor estaria ocioso.
No modelo serverless da AWS, o serviço responsável por executar funções sob demanda é o AWS Lambda. Uma Lambda Function é essencialmente um código que define o que fazer quando chamado. Quando o API Gateway recebe uma requisição HTTP proveniente do aplicativo Flutter, ele invoca a Lambda Function correspondente, passando como parâmetro todas as informações da requisição: o método HTTP, o caminho, os cabeçalhos, o corpo. A Lambda executa, produz uma resposta e encerra. Se dez requisições chegam simultaneamente, a AWS cria dez execuções paralelas da mesma função — sem que você precise configurar nada para isso.
A figura acima representa a arquitetura completa que você vai construir neste módulo. O aplicativo Flutter se comunica exclusivamente com o API Gateway via HTTPS. O API Gateway encaminha cada requisição para a Lambda Function correspondente. A Lambda consulta ou modifica o banco de dados RDS PostgreSQL e pode, adicionalmente, enfileirar mensagens no SQS para processamento assíncrono. Todas as execuções geram logs automaticamente no CloudWatch.
Vantagens e Considerações do Modelo Serverless
O modelo serverless traz vantagens concretas para um projeto do porte do aplicativo de delivery. A escalabilidade automática elimina a necessidade de estimar o pico de carga e dimensionar servidores de acordo: a AWS escala as funções automaticamente em resposta à demanda. O modelo de cobrança por uso significa que, durante a fase de desenvolvimento e nos períodos de baixo tráfego, os custos são próximos de zero. A operação simplificada libera você da responsabilidade de gerenciar sistema operacional, aplicar patches de segurança e monitorar processos — tarefas que, em um projeto real, consomem tempo significativo das equipes de infraestrutura.
No entanto, o modelo serverless também impõe certas restrições que você precisa conhecer. A primeira é o chamado cold start: quando uma função Lambda não foi invocada por algum tempo, a AWS desaloca o ambiente de execução. Na próxima invocação, ela precisa ser inicializada novamente, o que introduz uma latência inicial de algumas centenas de milissegundos. Para o aplicativo de delivery, esse atraso é raramente perceptível, mas é algo a considerar em aplicações que exigem latências muito baixas e consistentes. A segunda restrição é o tempo máximo de execução de uma Lambda Function, que é de quinze minutos — o suficiente para qualquer operação que o backend do delivery precisará fazer, mas insuficiente para processamentos de longa duração como geração de relatórios complexos ou transcodificação de vídeo.
A terceira restrição, e talvez a mais relevante para este módulo, é o gerenciamento de conexões com banco de dados. Em um servidor tradicional, um pool de conexões com o banco de dados é criado uma vez na inicialização da aplicação e reutilizado para todas as requisições subsequentes. Em funções Lambda, cada instância de execução tem seu próprio ambiente, o que pode resultar em um número alto de conexões abertas simultaneamente quando há muitas invocações paralelas. Você aprenderá uma estratégia para mitigar esse problema ainda neste módulo.
Seção 2 — API Gateway: a Porta de Entrada do Backend
O Amazon API Gateway é o serviço que expõe as suas Lambda Functions como endpoints HTTP acessíveis pela internet. Sem ele, o aplicativo Flutter não teria como invocar as funções Lambda diretamente — elas precisam de um ponto de acesso HTTP padronizado, com roteamento, controle de acesso e formatação de requisições e respostas.
A AWS oferece duas modalidades de API no API Gateway: a REST API e a HTTP API. A REST API é a modalidade mais completa, com suporte a recursos como validação de payload de requisição, geração de SDKs de cliente, cache de resposta por stage e transformação de requisição/resposta com templates de mapeamento. A HTTP API é uma versão mais simplificada e de menor custo, lançada mais recentemente, adequada para integrações mais diretas com Lambda quando as funcionalidades avançadas da REST API não são necessárias. Para o aplicativo de delivery, você usará a REST API, pois ela oferece mais controle sobre o comportamento de cada endpoint.
Conceitos Fundamentais do API Gateway
O API Gateway organiza os endpoints em torno de três conceitos que você precisa internalizar antes de configurar o primeiro endpoint: recurso, método e integração.
Um recurso no API Gateway corresponde a um segmento da URL. Para o backend do delivery, os recursos principais são /produtos, /pedidos, /categorias e /usuarios. Recursos podem ser aninhados hierarquicamente: /pedidos/{pedidoId}/itens representa os itens de um pedido específico, onde {pedidoId} é um parâmetro de caminho que o API Gateway extrai da URL e passa para a Lambda Function.
Um método é uma operação HTTP específica aplicada a um recurso. O método GET em /produtos lista todos os produtos. O método POST em /pedidos cria um novo pedido. O método GET em /pedidos/{pedidoId} busca um pedido específico. Cada combinação de recurso e método é configurada de forma independente no API Gateway e pode apontar para Lambda Functions diferentes.
Uma integração define como o API Gateway se conecta ao backend. Para este módulo, você usará sempre a integração do tipo Lambda Proxy. Nessa modalidade, o API Gateway encaminha a requisição completa para a Lambda como um objeto JSON estruturado, e a Lambda é responsável por construir a resposta completa — incluindo o código de status, os cabeçalhos e o corpo — também como um objeto JSON.
graph TD
subgraph "API Gateway"
A[/produtos - GET]
B[/produtos - POST]
C[/pedidos - GET]
D[/pedidos - POST]
E[/pedidos/{pedidoId} - GET]
F[/pedidos/{pedidoId}/itens - GET]
end
subgraph "Lambda Functions"
G[listar_produtos]
H[criar_produto]
I[listar_pedidos]
J[criar_pedido]
K[buscar_pedido]
L[listar_itens_pedido]
end
A --> G
B --> H
C --> I
D --> J
E --> K
F --> L
Criando a REST API no Console da AWS
Para criar a REST API do backend de delivery, acesse o console da AWS, navegue até o serviço API Gateway e selecione “Create API”. Escolha a opção “REST API” (não confundir com “REST API Private”, que é acessível apenas dentro de uma VPC) e clique em “Build”. Na tela de configuração, dê à API o nome delivery-api, mantenha o endpoint type como “Regional” (que é o mais adequado para APIs acessadas de uma região geográfica específica) e confirme a criação.
Com a API criada, você verá a interface de recursos, que inicialmente contém apenas o recurso raiz /. Para criar o recurso /produtos, selecione o recurso raiz e clique em “Create Resource”. No campo “Resource Name”, digite produtos. O campo “Resource Path” será preenchido automaticamente como /produtos. Clique em “Create Resource”.
Para adicionar o método GET ao recurso /produtos, selecione o recurso /produtos e clique em “Create Method”. No menu dropdown, selecione “GET” e confirme. Na configuração do método, selecione “Lambda Function” como tipo de integração, marque a caixa “Lambda Proxy Integration”, selecione a região onde sua função Lambda está (você criará a Lambda ainda neste módulo) e informe o nome da função. Confirme as permissões de invocação quando solicitado — o API Gateway precisará de permissão para invocar a função Lambda.
CORS no API Gateway
Quando o aplicativo Flutter rodando em ambiente de desenvolvimento faz requisições ao API Gateway, o mecanismo de Cross-Origin Resource Sharing (CORS) do navegador pode bloquear a requisição. Embora o Flutter em modo nativo (Android/iOS) não seja afetado pelo CORS, o CORS precisa ser configurado quando você testa o backend via ferramentas como o Postman ou cURL no modo web. Para habilitar o CORS em um recurso, selecione-o e use a opção “Enable CORS” disponível no menu “Actions”. O API Gateway adicionará automaticamente o método OPTIONS ao recurso e configurará os cabeçalhos necessários.
Deploy e Stages
Uma API no API Gateway só se torna acessível ao mundo externo após um deploy. Cada deploy é feito em um stage — um ambiente com seu próprio URL e configurações. Você pode ter um stage dev para desenvolvimento e testes, e um stage prod para o ambiente de produção. Para fazer o primeiro deploy, acesse o menu “Actions” e selecione “Deploy API”. Crie um stage chamado dev com uma descrição e confirme.
Após o deploy, a URL base da sua API terá o formato:
https://{api-id}.execute-api.{regiao}.amazonaws.com/{stage}
Por exemplo, https://abc123def4.execute-api.sa-east-1.amazonaws.com/dev. Esta será a URL base que você configurará no repositório HTTP do aplicativo Flutter.
Seção 3 — AWS Lambda: Funções como Serviço
A Lambda Function é onde a lógica de negócio do backend efetivamente reside. Cada função é um fragmento de código autônomo que recebe um evento de entrada, processa esse evento e retorna uma resposta. Para o backend do delivery, você escreverá funções em Python — a linguagem mais popular para Lambda Functions pela sua sintaxe limpa, pela rica biblioteca padrão e pelo excelente ecossistema de pacotes para integração com serviços AWS e bancos de dados.
A Estrutura de uma Lambda Function
Toda Lambda Function em Python é definida por uma função chamada handler. Por convenção, essa função recebe dois parâmetros: event e context. O parâmetro event é um dicionário Python que contém todas as informações da chamada que disparou a execução — no caso de uma integração Lambda Proxy com o API Gateway, ele conterá o método HTTP, o caminho, os parâmetros de consulta, os cabeçalhos e o corpo da requisição. O parâmetro context contém informações sobre o ambiente de execução da Lambda, como o nome da função, a memória disponível e o tempo restante até o timeout.
A função handler deve retornar um dicionário com pelo menos dois campos: statusCode (o código de status HTTP a ser retornado pelo API Gateway) e body (o corpo da resposta, sempre como string). Cabeçalhos de resposta são opcionais e são especificados no campo headers.
O esqueleto de uma Lambda Function que segue o padrão de integração Lambda Proxy é sempre o mesmo:
import json
def handler(event, context):
try:
# lógica de negócio aqui
resultado = {"mensagem": "operação realizada com sucesso"}
return {
"statusCode": 200,
"headers": {"Content-Type": "application/json"},
"body": json.dumps(resultado, ensure_ascii=False)
}
except Exception as erro:
return {
"statusCode": 500,
"headers": {"Content-Type": "application/json"},
"body": json.dumps({"erro": str(erro)}, ensure_ascii=False)
}Observe o uso de ensure_ascii=False no json.dumps. Esse parâmetro garante que caracteres especiais como acentos e cedilhas sejam preservados na serialização JSON em vez de serem escapados como sequências Unicode do tipo \u00e3.
Extraindo Dados do Evento
Quando o API Gateway invoca uma Lambda Function via integração Proxy, o objeto event tem uma estrutura previsível que você precisa conhecer para extrair corretamente os dados da requisição:
import json
def handler(event, context):
# método HTTP da requisição
metodo = event.get("httpMethod", "")
# parâmetros de caminho (ex: /pedidos/{pedidoId})
path_params = event.get("pathParameters") or {}
pedido_id = path_params.get("pedidoId")
# parâmetros de consulta (ex: /produtos?categoria=lanches)
query_params = event.get("queryStringParameters") or {}
categoria = query_params.get("categoria")
# corpo da requisição (sempre uma string JSON)
body_raw = event.get("body") or "{}"
body = json.loads(body_raw)
# cabeçalhos (úteis para extrair o token de autorização)
cabecalhos = event.get("headers") or {}
autorizacao = cabecalhos.get("Authorization", "")
# lógica específica do endpoint...
passCriando Lambda Functions para o Backend do Delivery
Para criar uma Lambda Function no console da AWS, navegue até o serviço Lambda e clique em “Create function”. Selecione “Author from scratch”, dê um nome descritivo (por exemplo, delivery-listar-produtos), selecione “Python 3.12” como runtime e escolha a arquitetura x86_64. Em “Execution role”, selecione “Create a new role with basic Lambda permissions” — você refinará as permissões desta role mais adiante.
Após criar a função, você verá o editor de código embutido no console. Para funções mais complexas (que dependem de pacotes externos como psycopg2), você precisará criar um pacote ZIP e fazer upload, mas comece com o editor embutido para as funções simples.
A seguir está a implementação da Lambda Function que lista todos os produtos disponíveis no cardápio do delivery. Observe que, neste momento, a função retorna dados estáticos — você a conectará ao banco de dados na Seção 4:
import json
PRODUTOS_MOCK = [
{"id": 1, "nome": "X-Burguer Clássico", "preco": 22.90, "categoria": "lanches", "disponivel": True},
{"id": 2, "nome": "Batata Frita Grande", "preco": 12.50, "categoria": "acompanhamentos", "disponivel": True},
{"id": 3, "nome": "Refrigerante 350ml", "preco": 6.00, "categoria": "bebidas", "disponivel": True},
]
def handler(event, context):
try:
query_params = event.get("queryStringParameters") or {}
categoria = query_params.get("categoria")
produtos = PRODUTOS_MOCK
if categoria:
produtos = [p for p in produtos if p["categoria"] == categoria]
return {
"statusCode": 200,
"headers": {"Content-Type": "application/json"},
"body": json.dumps(produtos, ensure_ascii=False)
}
except Exception as erro:
return {
"statusCode": 500,
"headers": {"Content-Type": "application/json"},
"body": json.dumps({"erro": str(erro)}, ensure_ascii=False)
}O Ambiente de Execução da Lambda e o Cold Start
Quando a AWS executa uma Lambda Function pela primeira vez (ou após um período de inatividade), ela inicializa um ambiente de execução: aloca memória, descompacta o código da função, importa o runtime Python e executa o código de nível de módulo (aquele que está fora da função handler). Esse processo de inicialização é o cold start. Nas invocações seguintes, enquanto o ambiente ainda estiver ativo, ele é reutilizado, e a execução começa diretamente a partir da chamada do handler — o chamado warm start.
Isso tem uma implicação importante para o design das suas funções: qualquer recurso que seja caro de inicializar — como uma conexão com o banco de dados — deve ser criado fora da função handler e no nível de módulo, de forma que seja reutilizado nas invocações subsequentes que usarem o mesmo ambiente de execução. Você verá essa técnica aplicada na próxima seção, quando criar as conexões com o RDS PostgreSQL.
Seção 4 — Integrando Lambda com RDS PostgreSQL
O RDS (Relational Database Service) é o serviço da AWS para bancos de dados relacionais gerenciados. No contexto do aplicativo de delivery, você usará uma instância RDS rodando PostgreSQL, que armazenará todas as entidades do sistema: produtos, categorias, usuários, pedidos e itens de pedido. O RDS gerencia automaticamente backups diários, atualizações de patch do mecanismo de banco de dados e failover para instâncias de standby.
Configuração de Rede: VPC e Security Groups
Para que uma Lambda Function possa se comunicar com uma instância RDS, ambas precisam estar configuradas corretamente do ponto de vista de rede. As instâncias RDS são sempre criadas dentro de uma VPC (Virtual Private Cloud) e, por padrão, não são acessíveis pela internet pública — apenas por recursos dentro da mesma VPC.
Quando você configura a Lambda Function para ser executada dentro de uma VPC, a AWS aloca interfaces de rede elásticas (ENIs) para a função, permitindo que ela se comunique com outros recursos da VPC, incluindo o RDS. Essa configuração é feita na seção “VPC” das configurações avançadas da Lambda Function: você seleciona a VPC, as subnets (idealmente subnets privadas, que não têm rota direta para a internet) e os security groups.
O security group da Lambda deve ter uma regra de saída (outbound) permitindo tráfego na porta 5432 (porta padrão do PostgreSQL) para o security group do RDS. O security group do RDS deve ter uma regra de entrada (inbound) permitindo tráfego na porta 5432 proveniente do security group da Lambda. Essa configuração garante que somente as Lambda Functions autorizadas possam se conectar ao banco de dados.
O Schema do Banco de Dados do Delivery
Antes de integrar Lambda e RDS, o schema do banco de dados precisa ser criado. Conecte-se à instância RDS usando um cliente PostgreSQL (como o psql ou o pgAdmin) e execute o seguinte script:
CREATE TABLE categorias (
id SERIAL PRIMARY KEY,
nome VARCHAR(100) NOT NULL UNIQUE
);
CREATE TABLE produtos (
id SERIAL PRIMARY KEY,
nome VARCHAR(200) NOT NULL,
descricao TEXT,
preco NUMERIC(10, 2) NOT NULL CHECK (preco >= 0),
categoria_id INTEGER NOT NULL REFERENCES categorias(id),
disponivel BOOLEAN NOT NULL DEFAULT TRUE,
criado_em TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
CREATE TABLE usuarios (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
nome VARCHAR(200) NOT NULL,
email VARCHAR(254) NOT NULL UNIQUE,
telefone VARCHAR(20),
criado_em TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
CREATE TABLE pedidos (
id SERIAL PRIMARY KEY,
usuario_id UUID NOT NULL REFERENCES usuarios(id),
status VARCHAR(30) NOT NULL DEFAULT 'pendente'
CHECK (status IN ('pendente','confirmado','em_preparo','saiu_para_entrega','entregue','cancelado')),
total NUMERIC(10, 2) NOT NULL DEFAULT 0,
criado_em TIMESTAMPTZ NOT NULL DEFAULT NOW(),
atualizado_em TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
CREATE TABLE itens_pedido (
id SERIAL PRIMARY KEY,
pedido_id INTEGER NOT NULL REFERENCES pedidos(id) ON DELETE CASCADE,
produto_id INTEGER NOT NULL REFERENCES produtos(id),
quantidade INTEGER NOT NULL CHECK (quantidade > 0),
preco_unitario NUMERIC(10, 2) NOT NULL
);
CREATE INDEX idx_pedidos_usuario ON pedidos(usuario_id);
CREATE INDEX idx_itens_pedido_pedido ON itens_pedido(pedido_id);Observe o uso de TIMESTAMPTZ (timestamp with time zone) em vez de TIMESTAMP. Em um sistema de delivery onde diferentes usuários podem estar em diferentes fusos horários, armazenar timestamps com informação de fuso horário evita uma categoria de bugs relacionados à conversão de horários. O tipo UUID para o identificador de usuários é uma boa prática de segurança, pois impede que atacantes tentem enumerar identificadores incrementais.
Lambda Layers: Incluindo Dependências Externas
O runtime Python da Lambda não inclui a biblioteca psycopg2, que é o driver PostgreSQL para Python. Para incluir dependências externas em uma Lambda Function, você tem duas opções: empacotar as dependências junto com o código da função em um arquivo ZIP, ou criar um Lambda Layer — uma camada reutilizável que pode ser compartilhada entre múltiplas funções.
A abordagem mais prática para começar é usar um Lambda Layer público que já contém o psycopg2 compilado para o ambiente Lambda. A AWS e a comunidade mantêm layers com psycopg2-binary que podem ser referenciados diretamente pelo ARN. Alternativamente, você pode criar seu próprio layer seguindo estes passos: em um ambiente Linux com Python 3.12 (ou usando Docker), instale o pacote com pip install psycopg2-binary -t python/, compacte a pasta python/ em um arquivo ZIP e faça o upload no console Lambda em “Layers”.
Após adicionar o layer à sua função Lambda, o psycopg2 estará disponível para importação como qualquer outro módulo Python.
Implementando as Lambda Functions com Acesso ao RDS
Com o schema criado e o driver psycopg2 disponível, é hora de implementar as Lambda Functions que realmente acessam o banco de dados. A estratégia de criar a conexão no nível de módulo — e reutilizá-la entre invocações que compartilham o mesmo ambiente de execução — é apresentada abaixo:
import json
import os
import psycopg2
import psycopg2.extras
# conexão criada fora do handler para ser reutilizada em warm starts
_conexao = None
def obter_conexao():
global _conexao
if _conexao is None or _conexao.closed:
_conexao = psycopg2.connect(
host=os.environ["DB_HOST"],
port=os.environ.get("DB_PORT", "5432"),
dbname=os.environ["DB_NAME"],
user=os.environ["DB_USER"],
password=os.environ["DB_PASSWORD"],
connect_timeout=5
)
return _conexao
def handler(event, context):
try:
query_params = event.get("queryStringParameters") or {}
categoria_id = query_params.get("categoriaId")
conn = obter_conexao()
with conn.cursor(cursor_factory=psycopg2.extras.RealDictCursor) as cur:
if categoria_id:
cur.execute(
"""
SELECT p.id, p.nome, p.descricao, p.preco,
c.nome AS categoria, p.disponivel
FROM produtos p
JOIN categorias c ON c.id = p.categoria_id
WHERE p.disponivel = TRUE AND p.categoria_id = %s
ORDER BY p.nome
""",
(categoria_id,)
)
else:
cur.execute(
"""
SELECT p.id, p.nome, p.descricao, p.preco,
c.nome AS categoria, p.disponivel
FROM produtos p
JOIN categorias c ON c.id = p.categoria_id
WHERE p.disponivel = TRUE
ORDER BY c.nome, p.nome
"""
)
produtos = cur.fetchall()
return {
"statusCode": 200,
"headers": {"Content-Type": "application/json"},
"body": json.dumps(
[dict(p) for p in produtos],
ensure_ascii=False,
default=str # serializa Decimal e outros tipos
)
}
except psycopg2.OperationalError as erro:
# força reconexão na próxima invocação
global _conexao
_conexao = None
return {
"statusCode": 503,
"headers": {"Content-Type": "application/json"},
"body": json.dumps({"erro": "serviço temporariamente indisponível"}, ensure_ascii=False)
}
except Exception as erro:
return {
"statusCode": 500,
"headers": {"Content-Type": "application/json"},
"body": json.dumps({"erro": str(erro)}, ensure_ascii=False)
}Preste atenção em dois detalhes importantes desta implementação. Primeiro, o uso de psycopg2.extras.RealDictCursor: esse cursor retorna cada linha do resultado como um dicionário Python em vez de uma tupla, o que simplifica enormemente a serialização para JSON. Segundo, a detecção de psycopg2.OperationalError que redefine _conexao para None: se a conexão com o banco de dados for perdida (por timeout, reinício do RDS ou qualquer outro motivo), a próxima invocação forçará a criação de uma nova conexão.
O default=str passado ao json.dumps garante que tipos como Decimal (retornado pelo psycopg2 para campos NUMERIC) e datetime sejam convertidos para string, evitando o erro Object of type Decimal is not JSON serializable.
A seguir está a implementação da Lambda que cria um novo pedido — uma operação mais complexa que envolve uma transação com múltiplas operações:
import json
import os
import psycopg2
import psycopg2.extras
_conexao = None
def obter_conexao():
global _conexao
if _conexao is None or _conexao.closed:
_conexao = psycopg2.connect(
host=os.environ["DB_HOST"],
port=os.environ.get("DB_PORT", "5432"),
dbname=os.environ["DB_NAME"],
user=os.environ["DB_USER"],
password=os.environ["DB_PASSWORD"],
connect_timeout=5
)
_conexao.autocommit = False
return _conexao
def handler(event, context):
try:
body = json.loads(event.get("body") or "{}")
usuario_id = body.get("usuarioId")
itens = body.get("itens", [])
if not usuario_id or not itens:
return {
"statusCode": 400,
"headers": {"Content-Type": "application/json"},
"body": json.dumps(
{"erro": "usuarioId e itens são obrigatórios"},
ensure_ascii=False
)
}
conn = obter_conexao()
with conn.cursor(cursor_factory=psycopg2.extras.RealDictCursor) as cur:
# 1. verifica disponibilidade de todos os produtos
ids_produto = [item["produtoId"] for item in itens]
cur.execute(
"SELECT id, preco, disponivel FROM produtos WHERE id = ANY(%s)",
(ids_produto,)
)
produtos_db = {row["id"]: row for row in cur.fetchall()}
for item in itens:
prod = produtos_db.get(item["produtoId"])
if not prod:
conn.rollback()
return {
"statusCode": 404,
"headers": {"Content-Type": "application/json"},
"body": json.dumps(
{"erro": f"produto {item['produtoId']} não encontrado"},
ensure_ascii=False
)
}
if not prod["disponivel"]:
conn.rollback()
return {
"statusCode": 422,
"headers": {"Content-Type": "application/json"},
"body": json.dumps(
{"erro": f"produto {item['produtoId']} não está disponível"},
ensure_ascii=False
)
}
# 2. calcula o total
total = sum(
produtos_db[item["produtoId"]]["preco"] * item["quantidade"]
for item in itens
)
# 3. cria o pedido
cur.execute(
"INSERT INTO pedidos (usuario_id, total) VALUES (%s, %s) RETURNING id",
(usuario_id, total)
)
pedido_id = cur.fetchone()["id"]
# 4. insere os itens
for item in itens:
cur.execute(
"""
INSERT INTO itens_pedido (pedido_id, produto_id, quantidade, preco_unitario)
VALUES (%s, %s, %s, %s)
""",
(pedido_id, item["produtoId"], item["quantidade"],
produtos_db[item["produtoId"]]["preco"])
)
conn.commit()
return {
"statusCode": 201,
"headers": {"Content-Type": "application/json"},
"body": json.dumps(
{"pedidoId": pedido_id, "total": float(total)},
ensure_ascii=False
)
}
except Exception as erro:
try:
obter_conexao().rollback()
except Exception:
pass
return {
"statusCode": 500,
"headers": {"Content-Type": "application/json"},
"body": json.dumps({"erro": str(erro)}, ensure_ascii=False)
}Essa implementação ilustra um padrão de transação completo: a função verifica a disponibilidade de todos os produtos antes de criar qualquer registro, calcula o total dinamicamente a partir dos preços atuais do banco de dados (e não dos preços informados pelo cliente, o que é fundamental por razões de segurança), e só confirma a transação com conn.commit() após todas as inserções terem sido bem-sucedidas. Qualquer falha dispara um rollback, garantindo que o banco de dados nunca fique em estado inconsistente.
Seção 5 — Variáveis de Ambiente e Segurança de Configuração
Se você observou com atenção o código das Lambda Functions da seção anterior, notou que as credenciais de conexão com o banco de dados — host, porta, nome do banco, usuário e senha — são lidas de variáveis de ambiente com os.environ["DB_HOST"] e similares. Esta é uma prática de segurança fundamental: nunca, sob nenhuma hipótese, escreva credenciais diretamente no código-fonte.
O motivo é simples e direto. Código-fonte vai para um repositório Git, e repositórios podem ser tornados públicos acidentalmente, podem ser acessados por colaboradores que não deveriam ter acesso às credenciais de produção, ou podem ser vazados em um incidente de segurança. Uma credencial embutida no código é uma vulnerabilidade permanente — mesmo que você a remova em um commit posterior, ela continua presente no histórico do repositório.
Para configurar variáveis de ambiente em uma Lambda Function, acesse a função no console da AWS, vá até a aba “Configuration” e selecione “Environment variables”. Adicione os pares chave-valor necessários:
| Chave | Exemplo de valor |
|---|---|
DB_HOST |
delivery-db.abc123.sa-east-1.rds.amazonaws.com |
DB_PORT |
5432 |
DB_NAME |
delivery |
DB_USER |
delivery_app |
DB_PASSWORD |
senha_segura_aqui |
As variáveis de ambiente das Lambda Functions são armazenadas criptografadas pela AWS usando chaves gerenciadas pelo serviço KMS (Key Management Service). Isso significa que mesmo que alguém obtenha acesso não autorizado à configuração da função, não conseguirá ler os valores em texto claro sem as permissões adequadas no KMS.
AWS Secrets Manager: a Solução Mais Robusta
Para um ambiente de produção real, a prática ainda mais segura é usar o AWS Secrets Manager em vez de variáveis de ambiente diretamente. O Secrets Manager armazena segredos como strings JSON criptografadas e permite rotação automática de credenciais — por exemplo, a senha do banco de dados pode ser trocada automaticamente a cada trinta dias sem nenhuma intervenção manual. Você pode configurar a Lambda para buscar o segredo no Secrets Manager durante o cold start e armazená-lo em variável global para reutilização nos warm starts, reduzindo a latência adicionada pela chamada à API do Secrets Manager.
A configuração do Secrets Manager está além do escopo deste módulo, mas é importante que você conheça sua existência para utilizá-lo em projetos profissionais.
Configuração de Diferentes Ambientes
Uma prática comum é manter duas conjuntos de variáveis de ambiente — um para o ambiente de desenvolvimento e outro para o de produção — e configurar cada Lambda Function com o conjunto correspondente. Você vai entender melhor como isso se encaixa na organização por stages do API Gateway na próxima seção.
Seção 6 — Stages no API Gateway: Ambientes de Desenvolvimento e Produção
Um stage no API Gateway é um snapshot de uma versão específica da sua API, acessível por uma URL própria. A separação entre stages de desenvolvimento e produção é uma prática de engenharia de software que permite testar mudanças no backend de forma segura sem impactar os usuários da aplicação em produção.
No contexto do backend do delivery, você vai trabalhar com dois stages ao longo da disciplina. O stage dev será usado durante o desenvolvimento e os testes locais — as Lambda Functions apontadas por ele se conectarão ao banco de dados de desenvolvimento, onde você pode inserir e modificar dados livremente sem preocupação. O stage prod será ativado para a Entrega Parcial e a Entrega Final do Projeto Integrador — ele aponta para Lambda Functions que se conectam ao banco de dados de produção, com dados consistentes e relevantes para a demonstração.
Stage Variables: Configuração por Ambiente
O API Gateway permite definir stage variables — pares chave-valor associados a um stage específico que podem ser referenciados na configuração do stage. A aplicação mais poderosa das stage variables é especificar dinamicamente o nome ou o ARN da Lambda Function que será invocada para cada método, permitindo que o mesmo recurso e método do API Gateway aponte para Lambda Functions diferentes dependendo do stage.
Por exemplo, ao configurar a integração do método GET em /produtos no API Gateway, em vez de especificar o nome fixo da função Lambda (delivery-listar-produtos), você usa uma referência à stage variable: ${stageVariables.listaProdutosFunction}. Em seguida, no stage dev, você define listaProdutosFunction = delivery-dev-listar-produtos, e no stage prod, você define listaProdutosFunction = delivery-prod-listar-produtos. Cada stage invocará uma função diferente, potencialmente conectada a bancos de dados diferentes.
Essa arquitetura, embora mais complexa de configurar inicialmente, torna a promoção de versões entre ambientes muito mais controlada: você pode testar exaustivamente no ambiente dev antes de apontar o stage prod para a mesma versão da função.
Exportando a URL do Stage para Uso no Flutter
Após cada deploy, o console da AWS exibe a URL do stage na parte superior da tela de stages. Essa URL é o que você vai configurar no repositório HTTP do aplicativo Flutter. Para facilitar a alternância entre dev e prod durante o desenvolvimento, uma boa prática é definir a URL base da API como uma constante no arquivo de configuração do projeto Flutter, de forma que trocar de ambiente exija apenas mudar esse único valor.
Seção 7 — Integrando o Aplicativo Flutter com o API Gateway
Todo o conhecimento que você adquiriu no Módulo 09 sobre consumo de APIs REST com o pacote http se aplica diretamente aqui. A diferença agora é que você é o autor do backend — você conhece cada endpoint, cada parâmetro e cada possível resposta porque você mesmo os definiu. Isso muda fundamentalmente a forma como você depura problemas de integração: quando algo não funciona, você pode verificar tanto o lado do Flutter quanto o lado da Lambda Function.
Atualizando o Repositório HTTP para Usar o API Gateway
No Módulo 09, você criou classes de repositório HTTP que abstraem o acesso à API. Agora, você simplesmente atualiza a URL base nos repositórios para apontar para o endpoint do API Gateway. A estrutura do código Flutter permanece idêntica — o que muda é apenas a origem dos dados.
A seguir, você vê a implementação atualizada do repositório de produtos, agora apontando para o API Gateway:
import 'dart:convert';
import 'package:http/http.dart' as http;
import '../../dominio/entidades/produto.dart';
import '../../dominio/repositorios/i_produto_repositorio.dart';
class HttpProdutoRepositorio implements IProdutoRepositorio {
final String _urlBase;
final http.Client _cliente;
HttpProdutoRepositorio({
required String urlBase,
http.Client? cliente,
}) : _urlBase = urlBase,
_cliente = cliente ?? http.Client();
@override
Future<List<Produto>> listarProdutos({String? categoriaId}) async {
final Map<String, String> params = {};
if (categoriaId != null) {
params['categoriaId'] = categoriaId;
}
final Uri uri = Uri.parse('$_urlBase/produtos').replace(
queryParameters: params.isNotEmpty ? params : null,
);
final http.Response resposta = await _cliente.get(
uri,
headers: {'Accept': 'application/json'},
);
if (resposta.statusCode == 200) {
final List<dynamic> dados = jsonDecode(resposta.body) as List<dynamic>;
return dados.map((json) => Produto.fromJson(json as Map<String, dynamic>)).toList();
}
throw Exception('Falha ao listar produtos: ${resposta.statusCode}');
}
@override
Future<void> criarPedido({
required String usuarioId,
required List<Map<String, dynamic>> itens,
}) async {
final Uri uri = Uri.parse('$_urlBase/pedidos');
final http.Response resposta = await _cliente.post(
uri,
headers: {
'Content-Type': 'application/json',
'Accept': 'application/json',
},
body: jsonEncode({
'usuarioId': usuarioId,
'itens': itens,
}),
);
if (resposta.statusCode != 201) {
final Map<String, dynamic> erro =
jsonDecode(resposta.body) as Map<String, dynamic>;
throw Exception(erro['erro'] ?? 'Falha ao criar pedido');
}
}
}import 'dart:convert';
import 'package:http/http.dart' as http;
import '../../dominio/entidades/produto.dart';
import '../../dominio/repositorios/i_produto_repositorio.dart';
class HttpProdutoRepositorio implements IProdutoRepositorio {
HttpProdutoRepositorio({required String urlBase, http.Client? cliente})
: _urlBase = urlBase,
_cliente = cliente ?? http.Client();
final String _urlBase;
final http.Client _cliente;
@override
Future<List<Produto>> listarProdutos({String? categoriaId}) async {
final uri = Uri.parse('$_urlBase/produtos').replace(
queryParameters: categoriaId != null ? {'categoriaId': categoriaId} : null,
);
final resposta = await _cliente.get(uri, headers: {'Accept': 'application/json'});
if (resposta.statusCode == 200) {
return (jsonDecode(resposta.body) as List)
.cast<Map<String, dynamic>>()
.map(Produto.fromJson)
.toList();
}
throw Exception('listarProdutos: ${resposta.statusCode}');
}
@override
Future<void> criarPedido({
required String usuarioId,
required List<Map<String, dynamic>> itens,
}) async {
final resposta = await _cliente.post(
Uri.parse('$_urlBase/pedidos'),
headers: {'Content-Type': 'application/json', 'Accept': 'application/json'},
body: jsonEncode({'usuarioId': usuarioId, 'itens': itens}),
);
if (resposta.statusCode != 201) {
final erro = jsonDecode(resposta.body) as Map<String, dynamic>;
throw Exception(erro['erro'] ?? 'criarPedido: ${resposta.statusCode}');
}
}
}Configuração da URL Base por Ambiente
Para facilitar a troca entre os environments de desenvolvimento e produção, defina a URL base em um arquivo de configuração centralizado. Uma abordagem simples e eficaz é criar uma classe AppConfig na camada core do projeto:
/// Configurações globais do aplicativo.
/// Para trocar entre dev e prod, altere a constante [_ambiente].
class AppConfig {
// Altere para 'prod' antes da entrega final
static const String _ambiente = 'dev';
static const Map<String, String> _urls = {
'dev': 'https://abc123def4.execute-api.sa-east-1.amazonaws.com/dev',
'prod': 'https://abc123def4.execute-api.sa-east-1.amazonaws.com/prod',
};
/// URL base da API a ser usada no ambiente atual.
static String get urlBaseApi {
return _urls[_ambiente]!;
}
/// Retorna true se o aplicativo está no ambiente de desenvolvimento.
static bool get isDesenvolvimento {
return _ambiente == 'dev';
}
}abstract final class AppConfig {
static const _ambiente = String.fromEnvironment('APP_ENV', defaultValue: 'dev');
static const _urls = {
'dev': 'https://abc123def4.execute-api.sa-east-1.amazonaws.com/dev',
'prod': 'https://abc123def4.execute-api.sa-east-1.amazonaws.com/prod',
};
static String get urlBaseApi => _urls[_ambiente]!;
static bool get isDesenvolvimento => _ambiente == 'dev';
}A versão otimizada usa String.fromEnvironment, que lê o valor da variável de ambiente APP_ENV definida no momento da compilação com o flag --dart-define=APP_ENV=prod. Isso significa que você pode compilar uma versão de desenvolvimento e uma versão de produção sem alterar nenhuma linha de código — apenas passando flags diferentes ao comando flutter run ou flutter build.
Tratamento de Erros Específicos do API Gateway
O API Gateway pode retornar alguns erros próprios antes de sequer chegar a invocar a Lambda Function. É importante que o repositório HTTP trate esses casos adequadamente:
Future<List<Produto>> listarProdutos({String? categoriaId}) async {
try {
final Uri uri = Uri.parse('${AppConfig.urlBaseApi}/produtos').replace(
queryParameters: categoriaId != null ? {'categoriaId': categoriaId} : null,
);
final http.Response resposta = await _cliente
.get(uri, headers: {'Accept': 'application/json'})
.timeout(const Duration(seconds: 15));
switch (resposta.statusCode) {
case 200:
final List<dynamic> dados = jsonDecode(resposta.body) as List<dynamic>;
return dados
.cast<Map<String, dynamic>>()
.map(Produto.fromJson)
.toList();
case 401:
throw const ErroAutenticacao('Token de acesso expirado ou inválido');
case 403:
throw const ErroPermissao('Sem permissão para listar produtos');
case 429:
throw const ErroLimiteTaxa('Muitas requisições. Aguarde e tente novamente.');
case 503:
throw const ErroServico('Serviço temporariamente indisponível');
default:
throw Exception('Erro inesperado: ${resposta.statusCode}');
}
} on TimeoutException {
throw const ErroRede('A requisição demorou demais. Verifique sua conexão.');
} on SocketException {
throw const ErroRede('Sem conexão com a internet.');
}
}Future<List<Produto>> listarProdutos({String? categoriaId}) async {
final uri = Uri.parse('${AppConfig.urlBaseApi}/produtos').replace(
queryParameters: categoriaId != null ? {'categoriaId': categoriaId} : null,
);
final resposta = await _cliente
.get(uri, headers: {'Accept': 'application/json'})
.timeout(const Duration(seconds: 15));
return switch (resposta.statusCode) {
200 => (jsonDecode(resposta.body) as List)
.cast<Map<String, dynamic>>()
.map(Produto.fromJson)
.toList(),
401 => throw const ErroAutenticacao('Token expirado'),
403 => throw const ErroPermissao('Sem permissão'),
429 => throw const ErroLimiteTaxa('Limite de requisições excedido'),
503 => throw const ErroServico('Serviço indisponível'),
_ => throw Exception('Erro: ${resposta.statusCode}'),
};
}Seção 8 — Amazon SQS: Processamento Assíncrono de Pedidos
Imagine o seguinte cenário: um cliente faz um pedido no aplicativo de delivery. O backend precisa salvar o pedido no banco de dados, enviar uma notificação para a cozinha, registrar o evento de pedido em um sistema de analytics e atualizar o estoque de ingredientes. Se a Lambda responsável por criar o pedido precisasse realizar todas essas ações de forma síncrona — esperando cada uma terminar antes de prosseguir — o cliente ficaria aguardando vários segundos por uma confirmação. O Amazon SQS resolve exatamente esse problema.
O SQS é um serviço de fila de mensagens que permite o desacoplamento entre produtores (quem envia mensagens) e consumidores (quem processa mensagens). A Lambda que cria o pedido executa apenas o que é sincronamente necessário — salvar o pedido no banco de dados e retornar o pedidoId para o cliente — e enfileira uma mensagem no SQS com os detalhes do pedido. Outras Lambda Functions, configuradas para serem disparadas pelo SQS, processam essa mensagem de forma assíncrona: enviam a notificação para a cozinha, atualizam o analytics, registram o evento de rastreamento.
Filas Standard vs FIFO
O SQS oferece dois tipos de filas. A fila Standard garante entrega pelo menos uma vez (at-least-once delivery), o que significa que, em casos raros, a mesma mensagem pode ser entregue mais de uma vez. A ordem das mensagens é aproximada, não garantida. A fila FIFO (First-In, First-Out) garante exatamente uma entrega e preserva a ordem de inserção das mensagens. Para o processamento de pedidos do delivery, a fila FIFO é a escolha adequada: você quer garantir que cada pedido seja processado exatamente uma vez e que a sequência de pedidos seja respeitada.
sequenceDiagram
actor C as Cliente
participant F as Flutter App
participant AG as API Gateway
participant L1 as Lambda criar-pedido
participant DB as RDS PostgreSQL
participant SQ as SQS (FIFO)
participant L2 as Lambda processar-pedido
C->>F: confirma pedido
F->>AG: POST /pedidos
AG->>L1: invoca (evento)
L1->>DB: INSERT pedidos + itens_pedido
DB-->>L1: pedidoId = 42
L1->>SQ: envia mensagem (pedidoId=42)
SQ-->>L1: MessageId
L1-->>AG: 201 {pedidoId: 42}
AG-->>F: 201 {pedidoId: 42}
F-->>C: "Pedido confirmado!"
Note over SQ,L2: Processamento assíncrono
SQ->>L2: dispara (mensagem)
L2->>DB: UPDATE pedidos SET status='confirmado'
L2->>L2: envia notificação para cozinha
O diagrama acima mostra exatamente o fluxo assíncrono que você vai implementar. O cliente recebe a confirmação do pedido em milissegundos — assim que o banco de dados registra o pedido e a mensagem é enfileirada. O processamento adicional acontece em paralelo, sem impactar a experiência do usuário.
Criando a Fila SQS e Configurando o Produtor
Para criar uma fila FIFO no console da AWS, navegue até o SQS, clique em “Create queue”, selecione “FIFO” e dê o nome delivery-pedidos.fifo (as filas FIFO devem ter o sufixo .fifo). Nas configurações, defina o visibility timeout como 30 segundos (o tempo que uma mensagem fica invisível para outros consumidores enquanto está sendo processada) e a message retention period como 4 dias.
Para enviar mensagens ao SQS a partir de uma Lambda Function, você usa o SDK boto3, que é o SDK oficial da AWS para Python e está disponível nativamente no ambiente Lambda sem necessidade de layers:
import json
import boto3
import os
sqs = boto3.client('sqs', region_name=os.environ.get('AWS_REGION', 'sa-east-1'))
FILA_URL = os.environ['SQS_PEDIDOS_URL']
def enviar_pedido_para_fila(pedido_id: int, usuario_id: str) -> None:
mensagem = {
"pedidoId": pedido_id,
"usuarioId": usuario_id,
"evento": "PEDIDO_CRIADO"
}
sqs.send_message(
QueueUrl=FILA_URL,
MessageBody=json.dumps(mensagem, ensure_ascii=False),
MessageGroupId="pedidos", # obrigatório em filas FIFO
MessageDeduplicationId=str(pedido_id) # garante idempotência
)O MessageGroupId agrupa mensagens para preservação de ordem dentro do grupo. O MessageDeduplicationId é um identificador único por mensagem que o SQS usa para rejeitar duplicatas durante um intervalo de desduplicação de cinco minutos. Ao usar o pedidoId como MessageDeduplicationId, você garante que o mesmo pedido nunca será processado duas vezes, mesmo que a Lambda produtora seja invocada por algum mecanismo de retry.
Configurando o Consumidor SQS
A Lambda processadora é configurada com um trigger SQS. No console Lambda, na seção “Function overview”, clique em “Add trigger”, selecione “SQS” e informe o ARN da fila criada. Configure o batch size como 1 (para processar um pedido por vez) e confirme.
A Lambda consumidora recebe um evento com uma lista de registros SQS, mesmo que o batch size seja 1:
import json
import os
import psycopg2
import psycopg2.extras
_conexao = None
def obter_conexao():
global _conexao
if _conexao is None or _conexao.closed:
_conexao = psycopg2.connect(
host=os.environ["DB_HOST"],
dbname=os.environ["DB_NAME"],
user=os.environ["DB_USER"],
password=os.environ["DB_PASSWORD"],
)
return _conexao
def handler(event, context):
for registro in event.get("Records", []):
try:
dados = json.loads(registro["body"])
pedido_id = dados["pedidoId"]
conn = obter_conexao()
with conn.cursor() as cur:
cur.execute(
"UPDATE pedidos SET status = 'confirmado', atualizado_em = NOW() WHERE id = %s",
(pedido_id,)
)
conn.commit()
# aqui você adicionará, nos próximos módulos:
# - envio de notificação push via FCM (Módulo 12)
# - registro em sistema de analytics
print(f"Pedido {pedido_id} processado com sucesso")
except Exception as erro:
print(f"Erro ao processar mensagem: {erro}")
# re-lançar a exceção faz com que a mensagem retorne à fila
raiseO re-lançamento da exceção é um comportamento importante: quando a Lambda consumidora lança uma exceção, o SQS devolve a mensagem à fila (após o visibility timeout expirar) para uma nova tentativa. Após um número configurável de falhas, a mensagem é movida para uma Dead Letter Queue (DLQ) — uma fila separada onde mensagens problemáticas aguardam inspeção manual.
Seção 9 — Monitoramento com CloudWatch
Ao contrário de uma aplicação tradicional onde você pode conectar um debugger e inspecionar variáveis em tempo real, uma Lambda Function em produção é uma caixa preta que você só pode observar de fora. O CloudWatch é o sistema de observabilidade da AWS que torna possível entender o que está acontecendo dentro das suas funções — através de logs, métricas e alarmes.
Logs Automáticos e Log Groups
Toda Lambda Function emite logs automaticamente para o CloudWatch. Cada função tem seu próprio log group, com o nome /aws/lambda/{nome-da-função}. Dentro do log group, cada instância de execução cria um log stream separado. Cada print e cada traceback de exceção não capturada aparece nesses logs, o que os torna a primeira ferramenta de diagnóstico quando algo dá errado.
Para acessar os logs no console AWS, navegue até CloudWatch, selecione “Log groups” no menu lateral e localize o log group da função desejada. Você pode visualizar os logs em tempo quase real usando o modo “Live Tail”.
É importante nomear as mensagens de log de forma informativa. Em vez de apenas print("erro!"), use print(f"[criar_pedido] Falha ao criar pedido {pedido_id}: {type(erro).__name__}: {erro}"). Mensagens de log bem estruturadas economizam horas de depuração.
Para structured logging — logs em formato JSON, que podem ser facilmente consultados com CloudWatch Logs Insights — você pode usar o módulo json para formatar as mensagens:
import json
import logging
logger = logging.getLogger()
logger.setLevel(logging.INFO)
def log_estruturado(nivel: str, mensagem: str, **contexto):
logger.log(
getattr(logging, nivel.upper()),
json.dumps({"mensagem": mensagem, **contexto}, ensure_ascii=False, default=str)
)
# uso:
log_estruturado("INFO", "pedido criado", pedido_id=42, usuario_id="usr_abc", total=35.40)Métricas e Alarmes
O CloudWatch coleta automaticamente métricas para cada Lambda Function:
- Invocations: número total de vezes que a função foi invocada no período
- Errors: número de invocações que resultaram em erro
- Duration: tempo de execução de cada invocação (em milissegundos)
- Throttles: número de vezes que a invocação foi bloqueada por exceder o limite de concorrência
- ConcurrentExecutions: número de instâncias da função executando simultaneamente
Você pode criar alarmes no CloudWatch que enviam notificações por e-mail (via SNS) quando uma métrica ultrapassa um limiar. Por exemplo, um alarme que dispara quando a taxa de erros de qualquer Lambda Function exceder 5% em um período de cinco minutos é uma forma eficaz de detectar problemas em produção antes que os usuários os reportem.
CloudWatch Logs Insights para Análise de Logs
O CloudWatch Logs Insights é um serviço de consulta que permite analisar os logs das Lambda Functions com uma linguagem de query específica. Por exemplo, para encontrar todos os erros ocorridos nas últimas 24 horas nas Lambda Functions do delivery:
fields @timestamp, @message
| filter @logStream like /delivery/
| filter @message like /ERROR/
| sort @timestamp desc
| limit 50
Essa ferramenta é especialmente útil para depurar problemas de produção onde você precisa correlacionar logs de múltiplas invocações para entender a sequência de eventos que levou a uma falha.
Seção 10 — Segurança e o Princípio de Menor Privilégio
Toda a infraestrutura que você criou até aqui — API Gateway, Lambda Functions, RDS, SQS — existe dentro de um ecossistema de segurança gerenciado pelo IAM, o serviço de gerenciamento de identidade e acesso da AWS. Entender o IAM não é opcional quando você trabalha com AWS: uma configuração incorreta de permissões pode deixar dados dos usuários expostos ou, inversamente, impedir que o sistema funcione corretamente.
Conceitos Fundamentais do IAM
O IAM organiza o controle de acesso em torno de três entidades principais: usuários, grupos e roles (funções). Um usuário IAM representa uma identidade humana ou um sistema externo que precisa interagir com os serviços AWS. Um grupo é uma coleção de usuários que compartilham o mesmo conjunto de permissões. Uma role (ou função IAM) é uma identidade que pode ser assumida temporariamente por um serviço AWS — como uma Lambda Function — para obter permissões específicas.
Cada Lambda Function tem uma execution role associada — uma role IAM que define o que a função pode fazer. Quando você criou as Lambda Functions neste módulo, a AWS criou automaticamente uma execution role básica que permite apenas escrever logs no CloudWatch. Para que uma Lambda Function possa enviar mensagens ao SQS, você precisa adicionar a permissão sqs:SendMessage à execution role dessa função. Para que ela possa ler segredos do Secrets Manager, você precisa adicionar secretsmanager:GetSecretValue.
O Princípio de Menor Privilégio
O princípio de menor privilégio — em inglês, principle of least privilege — é a diretriz que determina que cada identidade deve ter apenas as permissões estritamente necessárias para realizar sua função, e nenhuma permissão além disso. Aplicado às Lambda Functions do delivery, esse princípio se traduz em:
A Lambda que lista produtos (delivery-listar-produtos) precisa apenas de permissão para se conectar ao RDS e escrever logs no CloudWatch. Ela não deve ter permissão para modificar dados no banco, enviar mensagens ao SQS ou acessar outros serviços da AWS.
A Lambda que cria pedidos (delivery-criar-pedido) precisa de permissão para escrever no RDS e para enviar mensagens ao SQS. Ela não deve ter permissão para modificar produtos ou acessar dados de outros usuários diretamente.
A Lambda que processa pedidos da fila SQS (delivery-processar-pedido) precisa de permissão para ler mensagens do SQS e atualizar registros no RDS. Ela não deve ter permissão para criar novos pedidos ou acessar dados financeiros.
Essa granularidade de permissões limita o impacto de um eventual comprometimento de uma função: mesmo que um atacante consiga explorar uma vulnerabilidade em uma Lambda específica, ele terá acesso apenas ao subconjunto de recursos que aquela função pode acessar.
Configurando Permissões na Execution Role
Para adicionar permissões à execution role de uma Lambda Function, acesse a função no console AWS, vá até a aba “Configuration”, selecione “Permissions” e clique no link da execution role para abrir o IAM. Na tela do IAM, clique em “Add permissions” e selecione “Attach policies” para usar políticas gerenciadas pela AWS, ou “Create inline policy” para criar uma política específica.
Para a Lambda que envia mensagens ao SQS, uma inline policy adequada seria:
Observe que o campo Resource especifica o ARN exato da fila SQS — não "*" (que concederia acesso a todas as filas da conta). Essa especificidade é uma aplicação direta do princípio de menor privilégio: a função só pode enviar mensagens para a fila do delivery, e para nenhuma outra.
API Gateway e Autorização
O API Gateway oferece opções de autorização para proteger os endpoints. Para o backend do delivery, você usará API Keys para o estágio de desenvolvimento (uma chave simples que deve ser enviada no cabeçalho x-api-key) e, nos módulos subsequentes, você integrará a autorização com tokens JWT emitidos pelo fluxo OAuth 2.0 que estudará no Módulo 11.
Para proteger um endpoint com API Key no API Gateway, selecione o método desejado, vá até a seção “Method Request” e defina “API Key Required” como true. Em seguida, crie uma API Key no menu “API Keys” do API Gateway, associe-a a um Usage Plan e vincule o Usage Plan ao stage desejado. A partir desse momento, qualquer requisição ao endpoint sem o cabeçalho x-api-key: {chave} receberá uma resposta 403 Forbidden.
Seção 11 — Testando o Backend com Postman e Depurando com CloudWatch
Antes de integrar o backend ao Flutter, é fundamental testá-lo de forma isolada usando uma ferramenta como o Postman. Testar o backend de forma independente permite identificar problemas na lógica das Lambda Functions sem que o código Flutter introduza variáveis adicionais no processo de diagnóstico.
Estratégia de Testes Manuais
O fluxo de desenvolvimento que você vai adotar ao longo deste módulo e dos módulos seguintes segue uma progressão de três etapas. Primeiro, você cria e testa a Lambda Function de forma isolada usando o recurso de “Test Event” do próprio console Lambda — você define um evento JSON que simula o que o API Gateway enviaria, executa a função e verifica a resposta. Segundo, você faz o deploy da Lambda e configura o método no API Gateway, testando o endpoint com o Postman. Terceiro, você integra o endpoint ao repositório HTTP do Flutter.
Para criar um evento de teste no console Lambda que simula uma requisição GET em /produtos?categoriaId=2 enviada pelo API Gateway, use a seguinte estrutura JSON:
Para simular uma requisição POST em /pedidos, o evento seria:
{
"httpMethod": "POST",
"path": "/pedidos",
"pathParameters": null,
"queryStringParameters": null,
"headers": {
"Content-Type": "application/json"
},
"body": "{\"usuarioId\": \"550e8400-e29b-41d4-a716-446655440000\", \"itens\": [{\"produtoId\": 1, \"quantidade\": 2}, {\"produtoId\": 3, \"quantidade\": 1}]}",
"isBase64Encoded": false
}Interpretando os Logs do CloudWatch
Quando um teste falha, a primeira ação é consultar os logs do CloudWatch. Cada invocação gera um bloco de log com três eventos automáticos: START, que marca o início da execução com o request ID; o conteúdo da execução em si (seus prints e tracebacks de exceção); e END e REPORT, que informam a duração, a memória usada e o custo estimado.
Um traceback de exceção típico nos logs do CloudWatch tem a seguinte aparência:
[ERROR] psycopg2.OperationalError: could not connect to server: Connection timed out
Is the server running on host "delivery-db.abc123.sa-east-1.rds.amazonaws.com" (10.0.1.45) and accepting
TCP/IP connections on port 5432?
Traceback (most recent call last):
File "/var/task/lambda_function.py", line 42, in handler
conn = obter_conexao()
File "/var/task/lambda_function.py", line 18, in obter_conexao
_conexao = psycopg2.connect(...)
Neste caso, o problema é de rede: a Lambda não consegue se conectar ao RDS. Os motivos mais comuns são a Lambda não estar configurada para rodar na VPC correta, o security group do RDS não permitir tráfego proveniente do security group da Lambda, ou a função não ter as subnets corretas configuradas.
Seção 12 — Arquitetura Completa e Visão de Integração
Você estudou cada componente da arquitetura serverless do backend do delivery de forma isolada. Antes de partir para a prática do Projeto Integrador, é importante consolidar uma visão completa de como todos esses componentes se encaixam e se comunicam.
graph TB
subgraph "Aplicativo Flutter"
A1[Camada de Apresentação<br/>Widgets + Providers]
A2[Camada de Infraestrutura<br/>HttpProdutoRepositorio<br/>HttpPedidoRepositorio]
A3[Camada de Domínio<br/>Entidades + Contratos]
A1 --> A2
A2 --> A3
end
subgraph "AWS Cloud"
subgraph "API Gateway"
B1[REST API dev]
B2[REST API prod]
end
subgraph "Lambda Functions"
C1[listar-produtos]
C2[listar-categorias]
C3[criar-pedido]
C4[buscar-pedido]
C5[processar-pedido]
end
subgraph "Armazenamento"
D1[(RDS PostgreSQL)]
end
subgraph "Mensageria"
E1[SQS FIFO\ndelivery-pedidos.fifo]
end
subgraph "Observabilidade"
F1[CloudWatch Logs]
F2[CloudWatch Métricas]
end
end
A2 -->|HTTPS| B1
A2 -->|HTTPS| B2
B1 --> C1
B1 --> C2
B1 --> C3
B1 --> C4
C1 --> D1
C2 --> D1
C3 --> D1
C3 --> E1
E1 --> C5
C5 --> D1
C1 --> F1
C2 --> F1
C3 --> F1
C4 --> F1
C5 --> F1
O Fluxo de Dados do Pedido de Ponta a Ponta
Para solidificar sua compreensão, acompanhe o fluxo completo de dados quando um usuário faz um pedido no aplicativo de delivery:
O usuário seleciona os itens desejados na tela do cardápio e toca no botão de confirmar pedido. O widget TelaCarrinho chama o método confirmarPedido() do PedidoNotifier. O PedidoNotifier atualiza seu estado para carregando e notifica os widgets dependentes, que exibem o indicador de carregamento. O PedidoNotifier chama o método criarPedido() do HttpPedidoRepositorio.
O HttpPedidoRepositorio serializa os dados do pedido como JSON e faz uma requisição POST para https://{api-id}.execute-api.sa-east-1.amazonaws.com/dev/pedidos. A requisição trafega via HTTPS sobre a internet e chega ao API Gateway na região sa-east-1. O API Gateway verifica a API Key no cabeçalho, identifica o recurso e método correspondentes, e invoca a Lambda Function delivery-criar-pedido com um evento JSON contendo os dados da requisição.
A Lambda delivery-criar-pedido valida os dados recebidos, verifica a disponibilidade dos produtos no RDS, insere o pedido e seus itens em uma transação, obtém o pedidoId gerado, envia uma mensagem à fila SQS com o pedidoId, e retorna uma resposta com statusCode: 201 e o pedidoId.
O API Gateway encapsula a resposta da Lambda em uma resposta HTTP e a devolve ao aplicativo Flutter. O HttpPedidoRepositorio lê o código 201, desserializa o JSON e retorna os dados ao PedidoNotifier. O PedidoNotifier atualiza seu estado para sucesso e notifica os widgets, que navegam para a tela de confirmação do pedido.
Paralelamente, o SQS entrega a mensagem à Lambda delivery-processar-pedido, que atualiza o status do pedido para confirmado no banco de dados e executa as tarefas de notificação.
Todo esse ciclo, da confirmação pelo usuário até a resposta exibida na tela, acontece em menos de dois segundos na maioria dos cenários.
Seção 13 — Configurando o Ambiente AWS para Desenvolvimento
Antes de começar a construir qualquer recurso na AWS, é importante configurar seu ambiente de desenvolvimento de forma segura e organizada. Muitos estudantes cometem o erro de usar a conta raiz (root account) da AWS para todas as operações — essa prática representa um risco de segurança, pois a conta raiz tem permissões irrestrictas e, se comprometida, pode resultar na perda total do controle da conta e em cobranças inesperadas por recursos criados por um atacante.
Criando um Usuário IAM para Desenvolvimento
A prática correta é criar um usuário IAM dedicado para as operações de desenvolvimento e conceder a ele apenas as permissões necessárias. Para o projeto de delivery, você precisará de permissões nos serviços API Gateway, Lambda, RDS, SQS, CloudWatch Logs e IAM. A AWS oferece políticas gerenciadas para cada serviço — como AmazonAPIGatewayAdministrator, AWSLambda_FullAccess e AmazonRDSFullAccess — que podem ser anexadas ao usuário IAM de desenvolvimento.
Para criar o usuário IAM, navegue até o serviço IAM no console AWS com sua conta raiz. Clique em “Users” e depois em “Create user”. Dê ao usuário um nome que identifique seu propósito, como delivery-dev-admin. Na próxima tela, selecione “Attach policies directly” e anexe as políticas gerenciadas necessárias. Após confirmar a criação, acesse a aba “Security credentials” do usuário e crie um par de chaves de acesso (Access Key ID e Secret Access Key). Guarde essas chaves com cuidado — a Secret Access Key é exibida apenas uma vez.
Com as chaves criadas, configure o AWS CLI no seu computador. Se você ainda não instalou o AWS CLI, baixe o instalador para Windows no site oficial da AWS. Após a instalação, execute o comando de configuração e forneça suas credenciais:
aws configure
AWS Access Key ID [None]: AKIAIOSFODNN7EXAMPLE
AWS Secret Access Key [None]: wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY
Default region name [None]: sa-east-1
Default output format [None]: json
O AWS CLI armazenará essas configurações no arquivo ~/.aws/credentials. Com o CLI configurado, você pode testar a integração com a AWS diretamente da linha de comando — por exemplo, listando as funções Lambda da sua conta:
aws lambda list-functions --region sa-east-1
Organização de Recursos e Convenção de Nomes
Uma boa convenção de nomes para os recursos AWS do projeto de delivery facilita a identificação e evita confusão quando o número de recursos cresce. A convenção que você adotará é {projeto}-{ambiente}-{recurso}, onde {projeto} é delivery, {ambiente} é dev ou prod, e {recurso} descreve a função do recurso:
| Tipo de recurso | Nome no ambiente dev | Nome no ambiente prod |
|---|---|---|
| API Gateway | delivery-api |
delivery-api |
| Lambda — listar produtos | delivery-dev-listar-produtos |
delivery-prod-listar-produtos |
| Lambda — criar pedido | delivery-dev-criar-pedido |
delivery-prod-criar-pedido |
| Lambda — processar pedido | delivery-dev-processar-pedido |
delivery-prod-processar-pedido |
| RDS instance | delivery-dev-db |
delivery-prod-db |
| SQS queue | delivery-dev-pedidos.fifo |
delivery-prod-pedidos.fifo |
| Security group Lambda | delivery-dev-lambda-sg |
delivery-prod-lambda-sg |
| Security group RDS | delivery-dev-rds-sg |
delivery-prod-rds-sg |
Além da convenção de nomes, use tags da AWS para categorizar todos os recursos do projeto. As tags são pares chave-valor que você pode associar a qualquer recurso AWS. Defina pelo menos as tags Projeto: delivery, Ambiente: dev e Responsável: {seu-nome}. As tags facilitam a filtragem de recursos no console, a geração de relatórios de custo por projeto e a automação de operações como backup e limpeza de ambientes.
Configurando o RDS para Desenvolvimento
A criação de uma instância RDS PostgreSQL para o ambiente de desenvolvimento do delivery envolve algumas escolhas de configuração que merecem explicação. No console AWS, navegue até o RDS e clique em “Create database”. Selecione “Standard create” para ter controle completo sobre as configurações, e escolha PostgreSQL como engine. Para o ambiente de desenvolvimento, a edição “Free tier” é suficiente e não gera custos — ela inclui uma instância db.t3.micro com 20 GB de armazenamento e 750 horas por mês de uso gratuito durante o primeiro ano.
Na configuração da instância, defina o identificador como delivery-dev-db, crie um usuário master (recomenda-se delivery_admin) e uma senha forte. Na seção de conectividade, selecione a VPC padrão, configure a instância para não ser publicamente acessível (isso reforça a segurança, exigindo que o acesso passe pelas Lambda Functions dentro da mesma VPC) e crie um novo security group chamado delivery-dev-rds-sg.
Uma observação importante sobre o acesso ao banco de dados durante o desenvolvimento: como a instância RDS não é publicamente acessível, você não pode se conectar a ela diretamente do seu computador. Para executar migrações de schema, inserir dados de teste ou depurar consultas, existem duas opções. A primeira é criar temporariamente um endpoint público para a instância durante o desenvolvimento — o que é aceitável em um ambiente de desenvolvimento, mas jamais em produção. A segunda, e mais adequada, é usar o AWS Systems Manager Session Manager para criar um túnel SSH para a VPC sem precisar de acesso público. Para o escopo deste módulo, a primeira opção é suficiente; basta lembrar de desabilitar o acesso público quando terminar.
Seção 14 — Entendendo o Modelo de Custo da Arquitetura Serverless
Uma das vantagens do modelo serverless que você provavelmente ouviu mencionar é o custo reduzido. Mas o que exatamente isso significa na prática? Quanto o backend do delivery vai custar para manter funcionando? Entender o modelo de precificação de cada serviço que você está usando é uma habilidade que todo desenvolvedor que trabalha com cloud deve ter.
Precificação do AWS Lambda
O Lambda cobra por dois componentes: o número de requisições e o tempo de execução medido em GB-segundos. O primeiro componente é simples: a AWS oferece um nível gratuito de um milhão de requisições por mês, que nunca expira — mesmo após o período de um ano do nível gratuito geral. Acima do nível gratuito, o custo é de aproximadamente US$ 0,20 por milhão de requisições.
O segundo componente é calculado com base na memória configurada para a função e no tempo de execução: GB-segundo = (memória em MB / 1024) × (duração em segundos). A memória padrão de uma Lambda Function é 128 MB. Uma função com 128 MB que executa em 100 milissegundos consome 0,0125 GB-segundo por invocação. O nível gratuito inclui 400.000 GB-segundos por mês, e o custo acima disso é de aproximadamente US$ 0,0000166667 por GB-segundo.
Para ter uma noção concreta: se o aplicativo de delivery receber 10.000 pedidos por dia (um volume alto para um projeto acadêmico), cada pedido disparando em média 3 requisições Lambda com duração média de 200 ms e 128 MB de memória, o consumo mensal seria de 10.000 × 30 × 3 = 900.000 invocações e 10.000 × 30 × 3 × 0,0025 GB-s = 2.250 GB-segundos. Ambos ficam dentro do nível gratuito — o custo do Lambda seria zero.
Precificação do API Gateway
O API Gateway cobra por número de chamadas de API recebidas. Para a REST API, o custo é de aproximadamente US$ 3,50 por milhão de chamadas. O nível gratuito inclui um milhão de chamadas por mês durante os primeiros doze meses. Voltando ao exemplo anterior, 900.000 chamadas mensais ficam dentro do nível gratuito.
Precificação do RDS
O RDS é o componente de maior custo fixo da arquitetura. A instância db.t3.micro custa aproximadamente US$ 0,016 por hora no modo de pagamento por uso na região sa-east-1, o que dá cerca de US$ 11,52 por mês. O nível gratuito do primeiro ano cobre 750 horas por mês de uma instância db.t3.micro — o suficiente para manter uma única instância rodando continuamente sem custo durante o período de desenvolvimento.
O custo de armazenamento é adicional: 20 GB de armazenamento SSD de propósito geral custam aproximadamente US$ 2,30 por mês, e esse custo não está coberto pelo nível gratuito depois do primeiro ano.
Precificação do SQS
O SQS cobra por número de requisições de API. O nível gratuito inclui um milhão de requisições por mês indefinidamente. Acima disso, o custo é de US$ 0,40 por milhão de requisições para filas Standard e US$ 0,50 por milhão para filas FIFO. Para o volume do projeto de delivery, o SQS opera inteiramente dentro do nível gratuito.
Custo Total Estimado para o Projeto Integrador
Consolidando as estimativas acima, o custo mensal esperado para o backend do delivery durante o período de desenvolvimento é de aproximadamente zero dólares durante o primeiro ano, desde que a conta AWS tenha sido criada recentemente e o uso esteja dentro dos limites do nível gratuito. Após o primeiro ano, o custo mensal seria dominado pelo RDS, com cerca de US$ 14 por mês (instância + armazenamento) para uma única instância de desenvolvimento.
Essa análise reforça uma das principais vantagens da arquitetura serverless para projetos acadêmicos e startups em fase inicial: o custo operacional durante o desenvolvimento e os primeiros meses de produção é negligenciável, e ele escala proporcionalmente ao volume de uso — ao contrário de um servidor dedicado, cujo custo é fixo independentemente de haver ou não tráfego.
Seção 15 — Depurando Problemas Comuns na Integração Flutter-AWS
Mesmo com toda a teoria bem compreendida, a integração entre o aplicativo Flutter e o backend AWS inevitavelmente apresenta desafios práticos. Nesta seção, você vai conhecer os problemas mais comuns que desenvolvedores encontram ao realizar essa integração e as estratégias para resolvê-los de forma sistemática.
O Problema dos Erros 502 do API Gateway
Um dos erros mais frustrantes ao trabalhar com Lambda e API Gateway é o código de status 502 Bad Gateway. Ele ocorre quando a Lambda retorna uma resposta que o API Gateway não consegue processar corretamente — tipicamente porque a resposta não segue o formato esperado pela integração Lambda Proxy. O formato correto, como você viu na Seção 3, exige que a resposta seja um dicionário Python com pelo menos statusCode (um número inteiro) e body (uma string). Os erros mais comuns que geram um 502 são: retornar o corpo como um dicionário em vez de uma string (esquecer o json.dumps); retornar statusCode como uma string em vez de um inteiro; ou lançar uma exceção não capturada, que a AWS converte em uma resposta de erro que o API Gateway não consegue interpretar.
Quando você recebe um 502, o primeiro passo é consultar os logs do CloudWatch para a Lambda Function correspondente. Se a Lambda lançou uma exceção não capturada, o traceback completo estará nos logs. Se a Lambda retornou sem erro mas ainda assim o API Gateway respondeu com 502, ative o logging de execução do API Gateway — uma configuração que registra as respostas das integrações antes de serem processadas pelo API Gateway, o que permite identificar exatamente o que a Lambda retornou.
O Problema das Conexões Esgotadas no RDS
Quando múltiplas instâncias de Lambda Functions são executadas paralelamente, cada uma tentando manter sua própria conexão com o RDS, o banco de dados pode atingir o limite máximo de conexões simultâneas. Uma instância db.t3.micro suporta em torno de 80 a 100 conexões simultâneas. Com 100 instâncias paralelas de Lambda Functions, cada uma com uma conexão aberta, você atingiria esse limite e as novas tentativas de conexão falhariam com o erro too many connections.
A solução mais eficaz para esse problema em um ambiente serverless é o Amazon RDS Proxy, um serviço gerenciado que faz o pooling de conexões entre as Lambda Functions e o RDS. O RDS Proxy aceita milhares de conexões de Lambda Functions e as multiplexa em um número menor de conexões com o banco de dados, reduzindo dramaticamente o número de conexões abertas. Para o projeto de delivery no contexto desta disciplina, o volume de tráfego não chegará perto do limite de conexões, mas é importante que você conheça essa solução para projetos futuros.
Uma mitigação mais simples, adequada para o volume do projeto de delivery, é configurar a Lambda para fechar a conexão explicitamente ao final de cada execução em vez de mantê-la aberta para reutilização. Embora isso elimine o benefício do warm start para as conexões, garante que o número de conexões simultâneas nunca exceda o número de execuções paralelas ativas.
O Problema do CORS em Testes Web
Quando você testa o aplicativo Flutter compilado para web (usando flutter run -d chrome) e ele tenta se comunicar com o API Gateway, o navegador bloqueia a requisição devido ao CORS — Cross-Origin Resource Sharing. O navegador exige que o servidor inclua cabeçalhos específicos (Access-Control-Allow-Origin, Access-Control-Allow-Methods, Access-Control-Allow-Headers) na resposta para permitir requisições de origens diferentes.
Para resolver isso no API Gateway, você não precisa apenas habilitar o CORS no console — as Lambda Functions também precisam incluir os cabeçalhos CORS nas suas respostas. Ao usar a integração Lambda Proxy, o API Gateway passa os cabeçalhos da resposta da Lambda diretamente para o cliente, então você precisa incluí-los manualmente em cada resposta:
CABECALHOS_CORS = {
"Content-Type": "application/json",
"Access-Control-Allow-Origin": "*",
"Access-Control-Allow-Headers": "Content-Type,X-Api-Key,Authorization",
"Access-Control-Allow-Methods": "GET,POST,PUT,DELETE,OPTIONS"
}
def handler(event, context):
# ...
return {
"statusCode": 200,
"headers": CABECALHOS_CORS,
"body": json.dumps(resultado, ensure_ascii=False)
}Em produção, substitua "*" no cabeçalho Access-Control-Allow-Origin pelo domínio exato da sua aplicação web para evitar que qualquer origem possa fazer requisições ao seu backend. Como o aplicativo de delivery é distribuído como app nativo (Android/iOS), o CORS não é um problema em produção — o bloqueio de CORS é um mecanismo do navegador e não se aplica a clientes HTTP nativos.
O Problema dos Timeouts de Conexão na Primeira Invocação
Após configurar a Lambda para rodar dentro da VPC, você pode encontrar um timeout durante os primeiros cold starts. Esse problema ocorre porque a criação de ENIs (Elastic Network Interfaces) para uma Lambda recém-configurada pode levar alguns segundos extras. A AWS resolveu parcialmente esse problema ao passar a reutilizar ENIs entre invocações de diferentes Lambdas na mesma VPC, mas o primeiro cold start após uma mudança de configuração ainda pode ser mais lento.
Se você observar timeouts apenas na primeira invocação após um deploy, mas não nas subsequentes, esse é provavelmente o comportamento de inicialização de ENI. Aumente o timeout da Lambda para pelo menos 30 segundos durante o desenvolvimento para dar margem a esses cold starts extras. Após confirmar que o sistema está funcionando corretamente, você pode reduzir o timeout ao valor adequado para o tempo real de execução da função.
Seção 16 — Boas Práticas e Considerações Finais
Ao longo deste módulo, você construiu uma arquitetura serverless completa para o backend do delivery. Antes de concluir, é importante revisar algumas boas práticas que consolidam o que foi estudado e previnem problemas comuns que aparecem à medida que o sistema cresce.
A organização do código das Lambda Functions merece atenção especial à medida que o número de funções cresce. Evite o antipadrão de copiar e colar a função obter_conexao() em cada arquivo. Em vez disso, crie um módulo Python compartilhado chamado db.py que centraliza a lógica de conexão com o banco de dados. Esse módulo pode ser incluído em todas as funções como parte do pacote ZIP ou como um Lambda Layer de código compartilhado.
O tamanho das Lambda Functions impacta diretamente o tempo de cold start. Mantenha cada função focada em uma única responsabilidade — o princípio de responsabilidade única, que você conhece da programação orientada a objetos, se aplica igualmente aqui. Uma função que cria pedidos não deve também listar produtos; elas devem ser funções separadas, cada uma com seu próprio conjunto mínimo de dependências.
O timeout de cada Lambda Function deve ser calibrado para a operação que ela realiza, com uma margem de segurança adequada. Uma função que executa uma consulta simples ao banco de dados pode ter timeout de 10 segundos. Uma função que processa múltiplos itens de uma fila SQS pode precisar de até 5 minutos. Configurar timeouts adequados evita que funções presas em loops infinitos ou em operações de banco de dados bloqueadas consumam recursos desnecessariamente.
A idempotência das operações de escrita é uma consideração importante em sistemas distribuídos. Quando o Flutter reenvia uma requisição por timeout (usando os mecanismos de retry que você estudou no Módulo 09), o backend deve ser capaz de reconhecer e descartar a operação duplicada sem efeitos colaterais. No caso da criação de pedidos, o uso do pedidoId como MessageDeduplicationId no SQS já garante a idempotência no processamento assíncrono. Para a criação em si, uma estratégia é incluir um campo idempotencyKey no corpo da requisição — gerado pelo Flutter como um UUID — e verificar no banco de dados se um pedido com aquela chave já existe antes de inserir um novo.
Você construiu algo expressivo neste módulo. Partindo de uma aplicação Flutter que consumia APIs externas, você criou seu próprio backend serverless na AWS, com uma REST API exposta pelo API Gateway, Lambda Functions escritas em Python que se conectam a um banco de dados PostgreSQL gerenciado pelo RDS, processamento assíncrono via SQS e monitoramento centralizado no CloudWatch. Esse conjunto de tecnologias é o mesmo utilizado por startups e empresas de médio porte ao redor do mundo para construir backends escaláveis e de baixo custo operacional.
No Módulo 11, você adicionará uma camada de autenticação e segurança ao sistema, implementando o fluxo OAuth 2.0 para autenticar os usuários do delivery de forma segura. Os tokens JWT emitidos pelo provedor de identidade substituirão as API Keys como mecanismo de autorização do API Gateway, elevando o nível de segurança de toda a arquitetura.
Resumo do Módulo
Neste módulo, você percorreu um caminho que começou com o entendimento do modelo de computação serverless e culminou na implementação completa de um backend para o aplicativo de delivery. Os conceitos centrais que você internalizou foram o modelo de execução das Lambda Functions, com atenção especial à distinção entre cold start e warm start e suas implicações para o design de funções que reutilizam conexões de banco de dados; a estrutura do API Gateway como ponto de entrada HTTP para o backend, com seus conceitos de recursos, métodos, integração Lambda Proxy, stages e stage variables; a comunicação entre Lambda e RDS PostgreSQL usando o driver psycopg2, com transações explícitas para garantir a consistência dos dados do pedido; o uso de variáveis de ambiente para isolar credenciais do código-fonte; o processamento assíncrono com SQS FIFO, que desacopla a criação do pedido do processamento subsequente sem impactar a experiência do usuário; o monitoramento com CloudWatch, que fornece a observabilidade necessária para depurar e operar o backend em produção; e os princípios de segurança IAM, especialmente o princípio de menor privilégio aplicado às execution roles das Lambda Functions.
A integração do Flutter com o API Gateway reutilizou integralmente os padrões do Módulo 09 — o pacote http, o padrão Repository e o tratamento de erros por código de status — demonstrando como uma arquitetura bem definida torna a substituição da origem dos dados um processo natural e sem surpresas.
Entrega Parcial do Projeto Integrador
Este módulo coincide com o marco da Entrega Parcial do Projeto Integrador. Ao final desta semana, o seu grupo deve apresentar ao professor uma demonstração funcional da aplicação, abrangendo todos os conteúdos dos Módulos 01 a 10. A entrega inclui o repositório Git atualizado no GitHub com a estrutura de pastas correta, o backend AWS configurado e funcional (API Gateway + Lambda Functions + RDS), a integração Flutter-AWS operacional para pelo menos o fluxo principal da aplicação, e o diário de desenvolvimento atualizado com os registros de cada aula até o momento.
Esta é a oportunidade de consolidar o trabalho das dez semanas anteriores, receber feedback do professor e ajustar a direção do projeto para a reta final da disciplina.