← Gestão de Açaí
EM PRODUÇÃO 2025 – Atual · plataforma de estudos em uso diário

Nícia Track

Desenvolvido integralmente por Paulo. Migração completa de Render para AWS realizada em produção.

NÚMEROS
800 questões em 13 disciplinas
~3.171 linhas de testes automatizados
Render → AWS migração realizada em produção: EC2 + RDS + S3
1 usuária ativa uso diário na preparação para concursos e processos seletivos
STACK
Django PostgreSQL Docker Gunicorn Nginx AWS EC2 AWS RDS AWS S3 AWS IAM Cloudflare
O PROBLEMA

Plataforma de questões para estudos com conteúdo real e uma usuária ativa, em uso diário na preparação para concursos públicos e processos seletivos. O sistema foi iniciado no Render — solução adequada para o MVP. À medida que a plataforma cresceu, surgiram limitações práticas: instância hibernando em idle, sem suporte a armazenamento persistente de arquivos de mídia entre deploys, e necessidade de controle maior sobre a infraestrutura.

A decisão de migrar para AWS não foi tomada por modismo técnico. Cada componente da migração resolveu um problema concreto: EC2 elimina a hibernação, RDS externaliza o banco com backups gerenciados, S3 persiste os arquivos de mídia entre redeploys, e Cloudflare fica na frente gerenciando DNS e TLS.

O ângulo técnico deste caso: colocar em produção a pilha completa 'do DNS ao banco' — Cloudflare → Nginx → Gunicorn → Django → RDS — e entender o que quebra em cada camada.

ARQUITETURA
Do DNS ao banco: Cloudflare → Nginx → Gunicorn → Django → RDS / S3

Cloudflare gerencia DNS e termina TLS. EC2 roda um container Docker com Nginx (proxy reverso) na frente de Gunicorn servindo o Django. Banco de dados no RDS PostgreSQL (VPC separada, sem IP público). Arquivos de mídia no S3 via django-storages. IAM com política de mínimo privilégio para o bucket. Security Group do RDS aceita conexão apenas do Security Group da EC2 — sem acesso à internet. Settings separados por ambiente via variável de ambiente DJANGO_SETTINGS_MODULE. A decisão de separar migrations do CMD do Docker foi tomada após o primeiro incidente em produção.

DECISÕES
DECISÃO

Arquitetura em camadas desde o primeiro commit

Problema
Projetos Django que começam sem separação de camadas acumulam lógica de negócio nas views e nos models. Quanto mais o sistema cresce, mais difícil fica refatorar — e mais os testes ficam acoplados à camada HTTP.
Escolha
BaseModel abstrato com UUID como PK e campos created_at/updated_at herdados por todos os models. Settings separados por ambiente (base, development, production, testing) via DJANGO_SETTINGS_MODULE. Settings de testing usam SQLite em memória + MD5Hasher — em vez do bcrypt padrão — tornando a suite de testes cerca de 100x mais rápida sem perder cobertura de lógica.
Resultado
~3.171 linhas de testes sem dependência de infraestrutura externa. Suite roda localmente e na CI com tempo aceitável. Novos models herdam UUID PK e timestamps automaticamente.
DECISÃO

Deploy AWS em fases, não big bang

Problema
Migrar todos os componentes ao mesmo tempo — EC2, RDS, S3, Cloudflare, Security Groups, Docker — em um único momento cria múltiplos pontos de falha simultâneos. Quando algo quebra, fica difícil isolar a causa.
Escolha
Migração em fases: EC2 funcionando com SQLite antes de provisionar o RDS. RDS acessível antes de configurar Security Groups restritivos. S3 configurado e testado antes de apontar o DNS. Cada fase tem critério de aceitação antes de avançar para a próxima.
Resultado
Cada problema surgiu em uma camada de cada vez, isolado e diagnosticável. Nenhum rollback de múltiplas peças simultâneas necessário.
DECISÃO

Security Group do RDS aceita apenas o SG da EC2

Problema
Configuração inicial do RDS com Security Group aberto (0.0.0.0/0) para testar conectividade. Banco de produção com IP público e porta 5432 acessível pela internet.
Alternativas
Restringir por IP fixo da EC2 — fragiliza se a EC2 for recriada (o IP muda). Manter aberto e proteger por senha forte — segurança por obscuridade, não por design.
Escolha
Regra de entrada no SG do RDS que aceita conexão na porta 5432 apenas do Security Group da EC2, não de IPs individuais. Adicionado sslmode=require na string de conexão do Django. O banco não tem IP público — existe apenas dentro da VPC.
Resultado
RDS inacessível pela internet por design de rede, não por senha. Independente de qual EC2 substitua a atual, a regra de SG continua válida.
DECISÃO

Arquivos de mídia no S3, não no filesystem da EC2

Problema
Arquivos de mídia salvos no filesystem da EC2 são perdidos a cada redeploy do container ou recriação da instância. Descoberto na prática: após o primeiro redeploy em produção, as imagens das questões desapareceram.
Escolha
django-storages + boto3 com backend S3. MEDIA_URL aponta para o bucket. IAM com política de mínimo privilégio: o usuário da aplicação tem apenas s3:GetObject, s3:PutObject e s3:DeleteObject no bucket específico — sem acesso a outros recursos AWS.
Resultado
Arquivos de mídia persistem entre redeploys e recriações de instância. Bucket com política IAM restrita — blast radius limitado se as credenciais forem comprometidas.
DECISÃO

Migrations separadas do CMD de startup do container

Problema
CMD do Dockerfile rodava migrate antes de iniciar o Gunicorn. Durante o deploy, o container novo sobe antes do antigo ser derrubado. Com os dois rodando migrate no startup, as migrations disparavam em paralelo — causando race condition nas alterações de schema. Descoberto em produção quando duas instâncias subiram ao mesmo tempo.
Alternativas
Migrations manuais — elimina o risco mas requer intervenção humana a cada deploy. Entrypoint com lock distribuído — adiciona complexidade sem garantia em todos os provedores.
Escolha
Job separado de migrations que roda antes de subir o container da aplicação. CMD do container inicia apenas o Gunicorn. A separação torna explícito que migrations são uma operação de banco — não de aplicação — e que devem rodar exatamente uma vez por deploy.
Resultado
Nenhum race condition de migration desde a mudança. Restart de container não executa migration. Processo de deploy mais claro: migration → health check → tráfego.
O QUE QUEBROU E COMO RESOLVI
● INCIDENTE

Arquivos de mídia perdidos após redeploy

Sintoma
Após o primeiro redeploy em produção, todas as imagens vinculadas às questões exibiram erro 404. Os arquivos tinham sido salvos no filesystem da EC2 dentro do container — que foi substituído pelo novo deploy.
Causa
Filesystem de container é efêmero por design. MEDIA_ROOT apontava para um diretório local dentro do container. Arquivos não persistidos externamente são perdidos quando o container é recriado.
Correção
Migração dos arquivos existentes para S3 via AWS CLI. Configuração do django-storages com backend S3. A partir desse ponto, uploads vão direto para o bucket e sobrevivem a qualquer número de redeploys.
● INCIDENTE

Migrations rodando em paralelo na subida de container novo e antigo

Sintoma
Com o CMD do Dockerfile executando migrate no startup, quando o container novo subiu com o antigo ainda de pé, ambas tentaram aplicar as mesmas migrations ao mesmo tempo. Erro de lock no banco.
Correção
Separação das migrations em job dedicado, executado antes das réplicas subirem. CMD do container passou a iniciar apenas o Gunicorn. A ordem de execução ficou explícita e controlada.
● INCIDENTE

Security Group do RDS com acesso público durante a fase de testes

Sintoma
Para verificar conectividade durante a migração, o Security Group do RDS foi temporariamente aberto para 0.0.0.0/0. O banco ficou acessível pela internet com IP público durante parte do processo de configuração.
Correção
Substituição da regra de 0.0.0.0/0 pela referência direta ao Security Group da EC2. RDS sem IP público. sslmode=require na string de conexão. Feita imediatamente ao confirmar que a conectividade via SG funcionava corretamente.
RESULTADO

Plataforma com 800 questões em 13 disciplinas, operando em produção na AWS, com uma usuária em uso diário desde o deploy. ~3.171 linhas de testes automatizados com suite rápida graças ao MD5Hasher e SQLite em memória no ambiente de testes.

Migração Render → AWS concluída sem perda de dados. Infraestrutura com separação clara de responsabilidades: Cloudflare (DNS/TLS) → Nginx (proxy) → Gunicorn (WSGI) → Django → RDS (banco). Arquivos de mídia persistentes no S3. Security Group do RDS sem exposição à internet.

CAPTURAS
APRENDIZADO

Migração incremental — uma camada de cada vez, com critério de aceitação antes de avançar — é a única forma de diagnosticar o que quebra sem colapsar tudo ao mesmo tempo. E segurança por design de rede (SG do RDS só aceitando o SG da EC2) é mais confiável do que segurança por senha: elimina a superfície de ataque antes de precisar de credencial.