# Migração para Plataformas Stateful Cloud Native no AWS EKS

Migrar workloads stateful para plataformas cloud native não é apenas uma questão de containerização — é uma série de decisões de isolamento, consistência de dados e automação operacional que determinam se a plataforma sobrevive a produção. Neste artigo, percorro a jornada de uma plataforma financeira que saiu de VMs gerenciadas manualmente para um ambiente EKS multitenant com operadores Kubernetes, armazenamento persistente gerenciado e observabilidade de ponta a ponta.

- URL: https://fernando.moretes.com/blog/servicos-stateful-multitenant-com-isolamento-e-automacao

- Markdown: https://fernando.moretes.com/blog/servicos-stateful-multitenant-com-isolamento-e-automacao/article.md?lang=pt

- Published: 2026-05-26T12:00:00.000Z

- Category: Arquitetura de Soluções

- Tags: eks, stateful, kubernetes, multitenancy, migration, financial-grade, operators, observability

- Reading time: 11 min

- Source: [Stateful cloud native platforms](https://www.cncf.io/blog/)

---

Plataformas cloud native stateful multitenant são o novo campo minado da engenharia de infraestrutura. Quando o estado importa — e em sistemas financeiros ele sempre importa — a promessa de elasticidade do Kubernetes colide com a realidade de consistência, isolamento de dados entre tenants e falhas parciais que não têm rollback simples. Eu já vi essa migração ser feita de forma apressada, com equipes que trataram pods stateful como se fossem funções Lambda e pagaram o preço em incidentes de corrupção de dados e violações de isolamento entre clientes. O que apresento aqui é a jornada estruturada que eu recomendaria — e que executei — para mover uma plataforma financeira de VMs legadas para um EKS multitenant com garantias reais.

## O Ponto de Partida: VMs, Estado Implícito e a Ilusão de Controle

A plataforma original rodava em instâncias EC2 `r6i.4xlarge` com discos EBS gp2 montados diretamente, um banco de dados PostgreSQL por tenant em RDS Single-AZ, e uma camada de cache Redis gerenciada manualmente via scripts de bootstrap. O modelo funcionava — até não funcionar. Cada tenant tinha seu próprio conjunto de instâncias, o que garantia isolamento forte mas tornava o custo operacional proibitivo: 40 tenants significavam 40 stacks de infraestrutura, 40 pipelines de patch, 40 runbooks de failover. A equipe de operações passava mais tempo gerenciando infraestrutura do que entregando valor.

O estado era implícito em toda parte: sessões de usuário em memória local, filas de processamento em tabelas PostgreSQL sem particionamento adequado, e configurações de tenant espalhadas em arquivos `.env` versionados manualmente. Não havia um inventário confiável de qual instância servia qual tenant em um dado momento. Quando um nó falhava, o processo de recuperação envolvia SSH, verificação manual de logs e restauração de snapshots EBS — um MTTR médio de 47 minutos para incidentes de dados.

A pressão para migrar veio de dois lados: custo (a conta EC2 crescia linearmente com o número de tenants) e compliance (um audit de SOC 2 Type II identificou a falta de separação lógica auditável entre tenants como um achado de alto risco). A decisão de ir para EKS não foi tomada por modismo — foi tomada porque o modelo de operadores Kubernetes oferecia a automação de ciclo de vida que a equipe precisava para escalar sem crescer o headcount de operações proporcionalmente.

## A Jornada de Migração: Seis Fases com Decisões Reais

1. **Fase 1 — Inventário de Estado e Classificação de Workloads** — Antes de tocar em qualquer infraestrutura, mapeamos todos os pontos de estado: dados transacionais (PostgreSQL), cache de sessão (Redis), filas de jobs (tabelas PG), blobs de documentos (S3 já existente) e configuração de tenant (arquivos locais). Cada categoria recebeu uma classificação: stateless, stateful-ephemeral ou stateful-durable. Workloads stateless foram os primeiros a migrar. Stateful-durable foram os últimos — e exigiram operadores dedicados.

2. **Fase 2 — Design do Modelo de Multitenancy no EKS** — Avaliamos três modelos: cluster por tenant (forte isolamento, custo alto), namespace por tenant no mesmo cluster (custo eficiente, isolamento via RBAC e NetworkPolicy), e namespace por tenant com node pools dedicados por tier de criticidade. Escolhemos o terceiro: namespaces por tenant com node groups separados para tenants Gold (instâncias `r6i.2xlarge` dedicadas com taints) e Silver/Bronze (node group compartilhado com `m6i.xlarge`). Isso balanceou custo e isolamento de blast radius.

3. **Fase 3 — Persistência: EBS CSI, EFS e Aurora Serverless v2** — Para dados transacionais, migramos de RDS Single-AZ para Aurora PostgreSQL Serverless v2 com réplicas de leitura por tenant-group. O driver EBS CSI com `StorageClass` gp3 (3000 IOPS base, throughput de 125 MB/s) substituiu os volumes gp2 montados manualmente. Para artefatos compartilhados entre pods de um mesmo tenant (relatórios, uploads), usamos EFS com access points por namespace, garantindo isolamento de sistema de arquivos sem compartilhamento acidental entre tenants.

4. **Fase 4 — Operadores Kubernetes para Ciclo de Vida de Tenant** — Desenvolvemos um operador customizado (usando controller-runtime) que reagia a um CRD `TenantWorkspace`. Ao criar um `TenantWorkspace`, o operador provisionava: namespace com labels de tenant, ResourceQuota (CPU/memória/PVC), NetworkPolicy de isolamento, ServiceAccount com IRSA binding para o KMS key do tenant, e um segredo no AWS Secrets Manager referenciado via External Secrets Operator. O tempo de provisionamento de um novo tenant caiu de 4 horas (manual) para 8 minutos (automatizado).

5. **Fase 5 — Migração de Dados com Janela de Corte Controlada** — Usamos AWS DMS com replicação contínua (CDC) para sincronizar os bancos RDS legados com os novos clusters Aurora durante um período de shadow de 72 horas. O cutover foi executado tenant por tenant, começando pelos de menor volume transacional. Cada cutover seguia o padrão: (1) drenar conexões do app legado, (2) confirmar lag de replicação < 5 segundos, (3) promover Aurora como primário, (4) atualizar DNS interno via Route 53 weighted routing, (5) monitorar por 30 minutos antes de descomissionar a instância legada.

6. **Fase 6 — Observabilidade e Validação de SLOs** — Instrumentamos todos os serviços com OpenTelemetry SDK (traces e métricas), exportando para AWS X-Ray (traces) e CloudWatch Container Insights (métricas de infraestrutura). Um dashboard Datadog consolidou SLOs por tenant: disponibilidade de 99,9% (janela de 30 dias), latência P99 < 800ms para operações de escrita, e taxa de erro < 0,1%. Alertas de burn rate foram configurados para disparar quando o error budget consumia mais de 5% em 1 hora — muito antes de violar o SLO.

## O Modelo de Isolamento: Além do Namespace

Namespace por tenant é necessário, mas longe de suficiente em um ambiente financeiro. O namespace é uma fronteira de nomenclatura e RBAC — ele não impede que um pod mal configurado acesse a rede de outro tenant, nem que uma chave de criptografia seja compartilhada inadvertidamente.

O modelo de isolamento que implementamos tem quatro camadas:

**1. Isolamento de rede:** `NetworkPolicy` padrão nega todo tráfego ingress e egress dentro do cluster, exceto rotas explicitamente permitidas. Cada tenant só pode se comunicar com seu próprio namespace e com o namespace `platform-services` (que contém o proxy de saída e o serviço de autenticação). Usamos Calico como CNI para suporte a `GlobalNetworkPolicy`, que aplica regras antes mesmo das políticas de namespace.

**2. Isolamento de identidade:** Cada ServiceAccount de tenant tem uma IAM Role dedicada via IRSA (`eks.amazonaws.com/role-arn` annotation). A IAM Role tem uma condition `StringEquals` no `sts:AssumeRoleWithWebIdentity` que verifica o `sub` do token OIDC — garantindo que apenas pods do namespace correto possam assumir aquela role. Isso previne privilege escalation mesmo que um pod consiga criar um ServiceAccount em outro namespace.

**3. Isolamento de criptografia:** Cada tenant tem uma KMS Customer Managed Key (CMK) própria. Os volumes EBS, os segredos no Secrets Manager e os dados no S3 de cada tenant são criptografados com a CMK do tenant. A key policy da CMK usa `kms:ViaService` e `aws:SourceAccount` conditions para garantir que apenas chamadas originadas dos serviços corretos possam usar a chave.

**4. Isolamento de recursos:** `ResourceQuota` e `LimitRange` por namespace previnem que um tenant monopolize CPU ou memória do node group compartilhado. Para tenants Gold, os taints nos nodes dedicados garantem que nenhum pod de outro tenant seja agendado lá, mesmo em situações de pressão de scheduling.

## Arquitetura da Plataforma Stateful Multitenant no EKS

Fluxo de provisionamento e isolamento de tenant: do CRD TenantWorkspace ao estado persistente, com camadas de segurança e observabilidade.

### 🤖 EKS — Operator Plane

- TenantWorkspace Operator (controller-runtime) (compute)
- External Secrets Operator (security)

### 🔐 AWS — Security & Identity

- IRSA / OIDC per-tenant IAM Role (security)
- KMS CMK per-tenant key (security)
- Secrets Manager per-tenant secret (security)

### 📦 EKS — Tenant Namespace (Gold/Silver/Bronze)

- Namespace + NetworkPolicy + ResourceQuota (network)
- Stateful App Pods (gp3 EBS PVC) (compute)
- Redis StatefulSet ephemeral-stateful (data)

### 🗄️ AWS — Persistent Storage

- Aurora PostgreSQL Serverless v2 (per tenant-group) (data)
- EFS Access Point per-namespace (storage)
- S3 Bucket SSE-KMS per tenant (storage)

### 📊 Observability

- OTel Collector traces + metrics (compute)
- Datadog SLO dashboards (external)

### Fluxos

- admin -> operator: aplica CRD
- operator -> ns: provisiona namespace
- operator -> irsa: cria IAM binding
- operator -> eso: dispara sync de segredo
- eso -> secrets: lê segredo
- irsa -> kms: autoriza CMK
- ns -> app: contém pods
- app -> aurora: escrita transacional
- app -> cache: cache de sessão
- app -> efs: artefatos compartilhados
- app -> s3: blobs SSE-KMS
- app -> otel: traces + métricas
- otel -> dd: exporta SLO data

## Operadores Kubernetes: A Diferença entre Automação Real e Scripts com Kubectl

A decisão de investir em um operador customizado em vez de scripts Helm + kubectl foi a que mais gerou debate interno. O argumento contra era legítimo: operadores têm custo de desenvolvimento alto, introduzem um componente crítico no plano de controle e são difíceis de depurar quando o reconciliation loop entra em estado inconsistente.

O argumento a favor, que prevaleceu, foi baseado em três propriedades que scripts não têm:

**Reconciliação contínua:** Um script roda uma vez. Um operador observa continuamente o estado desejado versus o estado atual. Quando um engenheiro acidentalmente deletou uma `NetworkPolicy` de um tenant em produção (aconteceu), o operador a recriou em menos de 30 segundos — antes que qualquer tráfego não autorizado pudesse ocorrer. Com scripts, isso seria um incidente de segurança.

**Gestão de ciclo de vida completo:** O operador gerencia criação, atualização e deleção (com finalizers para garantir que dados não sejam deletados antes do backup). Um script de provisionamento raramente inclui lógica de deprovisioning segura — e quando inclui, é testada com muito menos rigor.

**Status e observabilidade nativos:** O CRD `TenantWorkspace` tem um campo `.status.conditions` com condições como `StorageProvisioned`, `NetworkPolicyApplied`, `SecretsSync`. Qualquer ferramenta que consuma a API do Kubernetes pode observar o estado de cada tenant em tempo real. Isso foi fundamental para o time de suporte: em vez de SSH em nodes, eles fazem `kubectl get tenantworkspace -n platform-ops` e veem o estado de todos os tenants em segundos.

O custo real do operador foi de aproximadamente 3 sprints de desenvolvimento e 1 sprint de hardening de testes de integração. O break-even operacional foi atingido no quarto mês, quando o número de tenants ultrapassou 60 e a complexidade de gerenciamento teria exigido contratar mais um engenheiro de operações.

## Antes e Depois: Métricas Operacionais da Migração

- **47min → 4min** — MTTR médio para incidentes de dados. Reconciliação automática do operador + alertas de burn rate eliminaram a fase de diagnóstico manual
- **4h → 8min** — Tempo de provisionamento de novo tenant. CRD TenantWorkspace + operador substituiu 4 horas de trabalho manual com scripts e aprovações
- **68% ↓** — Redução de custo de infraestrutura por tenant. Node groups compartilhados para Silver/Bronze + Aurora Serverless v2 (escala a zero em horários ociosos) vs. EC2 dedicado 24/7

## Persistência Stateful no Kubernetes: O Que Ninguém Te Conta Sobre PVCs em Produção

PersistentVolumeClaims com EBS têm uma limitação fundamental que frequentemente surpreende equipes em migração: um volume EBS só pode ser montado em um único node por vez (`ReadWriteOnce`). Isso tem implicações diretas em estratégias de deployment.

Com `Deployment` (não `StatefulSet`), um rolling update pode tentar criar o novo pod em um node diferente antes que o pod antigo libere o volume — resultando em um estado onde o novo pod fica em `ContainerCreating` indefinidamente, aguardando o volume ser desanexado. Em ambientes financeiros com janelas de manutenção curtas, isso pode ser catastrófico.

A solução que adotamos foi tripla: (1) usar `StatefulSet` para todos os workloads com PVCs EBS, o que garante que o pod antigo seja terminado antes do novo ser criado; (2) configurar `podManagementPolicy: Parallel` apenas para StatefulSets onde a ordem de startup não importa (cache Redis), mantendo `OrderedReady` para os que têm dependências de inicialização; (3) implementar `PodDisruptionBudget` com `minAvailable: 1` para garantir que drenos de node (durante upgrades do EKS) não removam o único pod de um tenant crítico sem um substituto pronto.

Para o caso de uso de artefatos compartilhados entre múltiplos pods do mesmo tenant (relatórios gerados assincronamente, por exemplo), EFS com `ReadWriteMany` foi a escolha correta — mas com uma ressalva importante: EFS tem latência de escrita significativamente maior que EBS para operações síncronas (tipicamente 1-5ms vs. 0.1-0.5ms para gp3). Qualquer path de código que escrevia no EFS de forma síncrona foi refatorado para escrita assíncrona via SQS, com o EFS sendo o destino final após processamento.

> **Riscos Críticos Que Quase Nos Custaram a Migração:** **1. Vazamento de estado entre tenants via cache compartilhado:** Em uma versão inicial, o Redis era compartilhado entre tenants Silver com prefixos de chave como separador. Um bug de prefixo em uma versão de código expôs dados de sessão de um tenant para outro por 12 minutos antes da detecção. A correção foi mover para Redis StatefulSets por tenant-group (não por tenant individual — custo proibitivo) com autenticação ACL por tenant.

**2. EBS volume attachment storm durante upgrades de node group:** Ao fazer rolling upgrade de um node group com 30 tenants, todos os volumes EBS tentaram ser desanexados e reanexados simultaneamente. O limite de 28 volumes EBS por instância `r6i.2xlarge` foi atingido, causando falhas de scheduling. A solução foi configurar `maxUnavailable: 1` no node group e usar PodDisruptionBudgets para forçar um ritmo controlado de migração de pods.

**3. Drift de configuração de NetworkPolicy:** Sem o operador de reconciliação ativo por 40 minutos durante uma janela de manutenção, um deploy manual de emergência criou um pod sem as labels corretas de tenant, que ficou fora das NetworkPolicies e com acesso irrestrito à rede interna do cluster. Isso reforçou a regra: o operador nunca pode ser desativado, mesmo em manutenção — ele deve ser atualizado com zero downtime via rolling update.

## Observabilidade em Plataformas Multitenant: O Tenant Como Dimensão de Primeira Classe

O erro mais comum que vejo em plataformas multitenant é tratar observabilidade como uma preocupação de infraestrutura — métricas de CPU, memória e latência agregadas no nível do cluster. Isso é inútil quando um cliente liga reclamando de lentidão: você não consegue isolar o problema sem dimensões de tenant.

A decisão arquitetural mais impactante na fase de observabilidade foi definir `tenant_id` como uma dimensão obrigatória em todas as métricas, traces e logs da plataforma. Isso foi implementado em três camadas:

**Aplicação:** O OTel SDK foi configurado com um `Resource` que inclui `tenant.id` e `tenant.tier` como atributos. Todos os spans herdam esses atributos automaticamente. No lado de métricas, usamos `exemplars` para vincular métricas de alta cardinalidade a traces específicos — fundamental para investigar P99 de um tenant específico.

**Infraestrutura:** CloudWatch Container Insights foi configurado com `enhanced observability` no EKS, que coleta métricas por pod com labels de Kubernetes. Um filtro de métricas no CloudWatch Logs Insights extrai `tenant_id` dos logs estruturados e cria métricas customizadas com essa dimensão.

**SLO por tenant:** No Datadog, cada tenant tem um SLO monitor independente. O burn rate alert usa uma janela de 1 hora (alerta rápido) e 6 horas (alerta de tendência). Quando o burn rate de 1 hora ultrapassa 14,4x (consumindo 2% do budget em 1 hora), um PagerDuty alert é criado com o `tenant_id` no título — o on-call sabe imediatamente qual tenant está impactado sem precisar investigar dashboards.

O custo de ingestão de métricas com alta cardinalidade foi uma preocupação real. A solução foi usar `histogram_quantile` no Prometheus (rodando como Managed Service via AMP) para métricas de latência, em vez de gauges por percentil — reduzindo a cardinalidade em 80% com precisão equivalente para SLOs.

## Avaliação pelos Pilares Well-Architected

- **security**: Isolamento em quatro camadas (rede, identidade, criptografia, recursos) com IRSA + KMS CMK por tenant. NetworkPolicy default-deny com Calico GlobalNetworkPolicy. Auditoria via CloudTrail de todas as chamadas KMS por tenant_id.
- **reliability**: StatefulSets com PodDisruptionBudgets garantem disponibilidade durante upgrades. Aurora Serverless v2 com réplicas de leitura e failover automático < 30s. Operador com reconciliação contínua previne drift de configuração.

> **Minha Nota de Curadoria:** Se eu fosse começar essa migração hoje, a única coisa que faria diferente seria investir no operador Kubernetes antes de migrar o primeiro tenant — não depois do quinto. A tentação de usar Helm charts e scripts bash para os primeiros tenants é enorme, e o débito técnico que isso cria é desproporcional. A lição mais dura que carrego dessa jornada é que isolamento em plataformas multitenant não é uma feature que você adiciona depois: é uma propriedade que precisa ser provada matematicamente antes do primeiro tenant de produção, com testes de penetração de namespace e simulações de falha de NetworkPolicy. Em ambientes financeiros, um vazamento de dados entre tenants não é um bug de severidade 2 — é um evento regulatório.

## Veredicto: Plataformas Stateful Cloud Native São Viáveis em Produção Financeira — Com as Condições Certas

A migração para uma plataforma stateful cloud native multitenant no EKS é tecnicamente viável e operacionalmente superior ao modelo de VMs por tenant — mas apenas se você aceitar que o investimento inicial é maior do que parece. O operador Kubernetes, o modelo de isolamento em quatro camadas e a observabilidade com dimensão de tenant não são otimizações opcionais: são pré-requisitos para operar com segurança em escala. Os números falam por si: 68% de redução de custo por tenant, MTTR de 47 minutos para 4 minutos, e provisionamento de 4 horas para 8 minutos são resultados reais — mas foram precedidos por 3 meses de trabalho de plataforma que não entregou nenhuma feature de negócio. Se a sua organização não tem tolerância para esse investimento upfront, o modelo de VMs por tenant, por mais caro que seja, é mais seguro do que uma plataforma Kubernetes mal isolada. A cloud native não é o destino certo para todo mundo — mas para quem faz o trabalho direito, a vantagem operacional composta ao longo do tempo é difícil de contestar.

## Referências

- [CNCF Blog: Stateful Cloud Native Platforms](https://www.cncf.io/blog/)
- [AWS EKS Best Practices Guide: Multi-tenancy](https://aws.github.io/aws-eks-best-practices/security/docs/multitenancy/)
- [AWS EKS EBS CSI Driver Documentation](https://docs.aws.amazon.com/eks/latest/userguide/ebs-csi.html)
- [AWS IRSA: IAM Roles for Service Accounts](https://docs.aws.amazon.com/eks/latest/userguide/iam-roles-for-service-accounts.html)
- [Aurora Serverless v2: How it works](https://docs.aws.amazon.com/AmazonRDS/latest/AuroraUserGuide/aurora-serverless-v2.html)
- [Kubernetes: Operator Pattern](https://kubernetes.io/docs/concepts/extend-kubernetes/operator/)
- [External Secrets Operator](https://external-secrets.io/latest/)
- [OpenTelemetry: Resource Semantic Conventions](https://opentelemetry.io/docs/specs/semconv/resource/)
