Migrar production para o EKS com convergência de dados para a ibft-vpc

TLDR: Move o compute de production das 6 EC2 para o EKS e converge os dados para o modelo do projeto (Aurora + ElastiCache na ibft-vpc, subnets privadas), com carga via pg_dump em janela curta e cutover por DNS.

Contexto

Staging já roda 100% no EKS (pipelines v2/v3 + ward + terraform workspace). Production ainda roda em 6 EC2 t4g.medium com supervisor, deploy via SSH na branch production. A auditoria (fase 0, 2026-08-06) mostrou que os dados já estão na AWS, conta 675845727614, mas na VPC legada vpc-01c1b3deba1dd05a5 (172.31.0.0/16), fora do modelo do projeto:

  • Aurora PostgreSQL 17.7 onion-db-production (writer + reader db.t4g.medium) atrás do RDS Proxy onion-proxy (writer + endpoint read-only), db onion, 1.5 GB
  • ElastiCache Serverless Valkey 8 onion (rediss, ~139k chaves, snapshot retention 0)
  • SQS com filas onion-production* — serviço regional, não preso à VPC
  • Segredos no SSM /ONION/PRODUCTION/BACKEND/ (24 params) via config/ssm_loader.py + instance role

Decisão: convergir para o modelo do projeto — Aurora e ElastiCache novos na ibft-vpc (vpc-0047c8135a4f0a0c4, subnets privadas, peering com o EKS já ativo), como staging. Evita peering/SGs de transição para a VPC legada. O SQS é reusado como está (regional). O banco pequeno (1.5 GB) torna a janela de migração curta.

Mecanismo de carga: pg_dump | psql (padrão já provado na migração de staging), não snapshot/restore. Motivo: restore de snapshot Aurora cria um cluster novo a cada sync — o cluster do terraform não poderia ser criado/validado antes da janela. Com pg_dump, o cluster de production nasce vazio pelo terraform, é ensaiado com dados reais dias antes (dump do endpoint read-only, sem impacto), e a janela final é só o dump+load (~10 min para 1.5 GB). Snapshot manual do cluster legado é tirado antes da janela como artefato de rollback.

Objetivos

  • Workspace terraform production aplicável sem conflitar com staging
  • Aurora + ElastiCache de production na ibft-vpc com perfil de produção (sem auto-pause, deletion_protection, prevent_destroy)
  • App sobe no k8s com o contrato de env alinhado (hoje o production.py crasharia)
  • Deploy por promoção de tag validada em staging (stamp → delivery)
  • Migração de dados em janela curta e cutover por DNS, com rollback definido

Fora de escopo

Os caches checkout/websocket e o db-cluster-ibft da VPC legada são de outros serviços — não são tocados.

Mudanças

Parte 1 — Terraform: workspace production

.infra/terraform/network.tf - Condicionar peering/rotas (EKS↔ibft-vpc) a local.environment == "staging" — já vivem no state de staging e servem às duas pontas; production não deve recriá-los

.infra/terraform/database.tf - Perfil por ambiente no cluster Aurora: - production: engine_version = "17.7", min_capacity = 1 (sem auto-pause), max_capacity = 8, deletion_protection = true, skip_final_snapshot = false + final_snapshot_identifier, lifecycle { prevent_destroy = true }, backup retention ≥ 7 dias - staging mantém o perfil atual - Adicionar instância reader em production (onion-cluster-reader-production-1) — paridade com o legado (endpoints writer/reader alimentam DATABASE_URL_FULLACCESS/READONLY) - Decisão em aberto (não bloqueia): RDS Proxy na frente do Aurora novo. O legado usa proxy (gunicorn/gevent gera muitas conexões); staging não usa. Começar sem proxy, com CONN_MAX_AGE conservador, e adicionar aws_db_proxy se o Datadog mostrar pressão de conexões

.infra/terraform/cache.tf - Perfil por ambiente: production com engine = "valkey" (paridade com o legado) e limites maiores (ex.: 5 GB / 5000 ECPU — validar contra o CloudWatch do cache atual antes do apply); staging mantém

.infra/terraform/secrets.tf / variables.tf - Adicionar ao secret as chaves que faltam para o production.py: CELERY_SQS_BASE_URL, SENTRY_DSN_KEY, EMAIL_*, MANYCHAT_API_TOKEN, ENABLE_MANYCHAT_NOTIFICATIONS, UNSPLASH_*, WEBHOOKS_CHECKOUT_NOTIFICATIONS_TOKEN, ANALYTICS_S3_BUCKET (valores copiados do SSM para o ward) - Credencial AWS para os pods: IAM user onion-backend-production (conta onion) com permissões SQS (filas onion-production*) + S3 (statics/analytics), chaves no secret como AWS_ACCESS_KEY_ID/AWS_SECRET_ACCESS_KEY (kombu e django-storages usam a cadeia default do boto)

.infra/terraform/dns.tf - Os records api/dash/download.onovoinconsciente.com.br já existem no Cloudflare apontando para o legado: fazer terraform import no workspace production. O apply que troca o content para var.ingress_hostname é o cutover — só rodar na janela (parte 5)

Ward (infra/production.ward) - Adicionar: TF_VAR_aws_db_password, TF_VAR_ingress_hostname, TF_VAR_celery_sqs_base_url, TF_VAR_sentry_dsn_key, TF_VAR_email_*, TF_VAR_manychat_*, TF_VAR_unsplash_*, TF_VAR_webhooks_checkout_notifications_token, TF_VAR_analytics_s3_bucket e as chaves do IAM user novo

Parte 2 — App: alinhar o contrato de env do production.py

config/settings/production.py - Ler os nomes do contrato k8s com fallback para os nomes legados do SSM (transição segura — a EC2 continua funcionando até o desligamento): - DATABASE_URL_FULLACCESS→DB_URL, DATABASE_URL_READONLY→DB_URL_READER, REDIS_URL→ELASTICACHE_REDIS_URL - Firebase: aceitar FIREBASE_CREDENTIALS_JSON (contrato k8s/staging) além de FIREBASE_CREDENTIALS_PATH - config/ssm_loader.py permanece — sem instance role no EKS ele falha com log e segue (verificar que a exceção é engolida mesmo sem credencial)

Parte 3 — Manifests: overlay production

.infra/k8s/overlays/production/ - patches/ingress.yaml: hosts api/dash/download.onovoinconsciente.com.br (espelhar staging) - Envs que faltam nos deployments (base ou overlay): CELERY_SQS_BASE_URL, SENTRY_DSN_KEY, EMAIL_*, MANYCHAT_*, UNSPLASH_*, WEBHOOKS_*, ANALYTICS_S3_BUCKET via secret - CELERY_QUEUES dos workers com os nomes onion-production* (conferir com as filas SQS reais) - Réplicas iniciais de worker* e beat = 0 no overlay (sobem só no cutover — evita consumo concorrente das filas SQS e beat duplicado com o legado)

Parte 4 — CI: deploy por promoção de tag

.github/workflows/deploy-production.yml (substituir conteúdo legado) - workflow_dispatch com input da tag já validada em staging: require-tag → stamp (target production) → delivery (environment production, environment_url https://api.onovoinconsciente.com.br, run_migrate: true), secrets explícitos como no staging - pylint-and-tests.yml / workflows legados permanecem até o descomissionamento (parte 6)

Parte 5 — Migração de dados e cutover (janela controlada)

Ensaio (dias antes, sem impacto):

  1. terraform apply de production (cria Aurora vazio, cache, namespace, secret; DNS ainda não flipado)
  2. Job k8s de pg_dump do endpoint read-only do proxy legado | psql no Aurora novo (mesmo padrão da migração de staging)
  3. Deploy da tag no k8s (workers/beat a 0), validar web via port-forward com dados reais (/health, login no dash, smoke read-only)
  4. Pré-check de migrations: diff entre a branch production (código nas EC2) e a tag a ser deployada; migrations pendentes precisam ser backward-compatible

Janela:

  1. Snapshot manual do cluster legado (artefato de rollback)
  2. Flush do buffer de progresso do Valkey para o banco (mecanismo existente) e parar workers/beat/web no supervisor das EC2 (write freeze)
  3. TRUNCATE/recreate do schema no Aurora novo + pg_dump | psql final (~10 min para 1.5 GB)
  4. delivery roda o migrate; escalar workers/beat no k8s; validar via port-forward
  5. terraform apply que flipa o DNS para o ALB do EKS. O cache Valkey novo começa frio (aceito; o buffer foi persistido no passo 2)
  6. Observar: Datadog/Sentry, filas SQS drenando, latência de queries

Rollback (enquanto não houver escrita significativa no Aurora novo): - Reverter DNS + religar o supervisor nas EC2 (dados legados intactos). Depois de escrita significativa, o rollback exige dump reverso — por isso a observação intensiva na primeira hora

Parte 6 — Descomissionamento (pós-observação, ≥ 48h estáveis)

  • Parar gunicorn/supervisor nas EC2; terminar as instâncias após período extra
  • Remover deploy-production.yml legado (SSH), deploy-staging-legacy.yml, pylint-and-tests.yml, scripts/deploy-*.sh e a branch production
  • Após o período de retenção: destruir o cluster Aurora legado onion-db-production, o RDS Proxy onion-proxy e o cache Valkey onion da VPC legada. Manter o snapshot manual e o path do SSM por um ciclo antes de apagar
  • Avaliar a remoção dos fallbacks legados no production.py

Como verificar

  • make terraform.production.plan limpo (sem tocar recursos de staging) e apply verde
  • De um pod no namespace onion-backend--production: psql no Aurora novo e PING no Valkey respondem; \dt mostra todas as tabelas após a carga
  • Web validado por port-forward antes do DNS; após o cutover, api/dash/download.onovoinconsciente.com.br respondem pelo ALB do EKS (TLS ok)
  • Filas SQS sendo consumidas pelos workers do k8s; beat agendando sem duplicidade; Datadog/Sentry sem novos erros; contagem de linhas por tabela bate entre legado e novo
  • Rollback ensaiado: DNS revertido volta a servir pelas EC2

Documentação

  • Criar learning com os gotchas da janela (write freeze, ordem workers/beat, DNS), contrato de env e sizing de Aurora/Valkey
  • reference/infrastructure/database_url.md — atualizar se o contrato de DATABASE_URL mudar