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.