Nícia Track
Desenvolvido integralmente por Paulo. Migração completa de Render para AWS realizada em produção.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.