Design Doc: EKS Multi-Tenant em Escala — Karpenter, Isolamento e Custo
Ouvir estudo
gerado ao ouvirGerado apenas no primeiro play
Com tecnologia Amazon Polly + OmniVoice
Este documento descreve a arquitetura de uma plataforma EKS multi-tenant para dezenas de equipes, cobrindo isolamento de workloads via namespaces, network policies e RBAC, autoscaling e consolidação com Karpenter usando Spot, e alocação de custo por tenant via Kubecost. O objetivo é operar com segurança, previsibilidade de custo e eficiência de infraestrutura sem sacrificar a autonomia das equipes.
Rodar dezenas de tenants num único cluster EKS é uma aposta de eficiência que só funciona se isolamento, custo e escala forem tratados como cidadãos de primeira classe desde o dia zero — não como ajustes tardios.
O Problema
Plataformas internas de Kubernetes crescem de forma orgânica. Uma equipe cria um cluster, outra pede acesso, e em dezoito meses você tem quarenta times compartilhando infraestrutura que foi desenhada para cinco. O resultado típico é um cluster onde ninguém sabe exatamente o que custa o quê, workloads críticos competem com jobs de batch por nós, e o blast radius de um namespace mal configurado pode afetar vizinhos.
O cenário que este documento aborda é uma plataforma EKS gerenciada por uma equipe de plataforma central, servindo entre 30 e 60 times de produto. Cada time é um tenant: tem seus próprios namespaces, repositórios, pipelines e, idealmente, sua própria conta de custo. O cluster precisa escalar de forma reativa e econômica, manter isolamento forte o suficiente para satisfazer auditoria de segurança, e produzir dados de custo granulares o suficiente para showback ou chargeback.
Os três vetores de falha mais comuns nesse modelo são: (1) isolamento insuficiente — um tenant consome recursos de outro por ausência de quotas ou network policies, ou pior, consegue escalar privilégios via RBAC mal configurado; (2) custo invisível — sem labels consistentes e ferramentas de alocação, a fatura da AWS vira um número agregado que ninguém consegue contestar ou otimizar; (3) autoscaling lento ou caro — Cluster Autoscaler com node groups fixos não acompanha picos heterogêneos de workload e tende a superprovisionamento por segurança. Karpenter resolve o terceiro vetor, mas precisa de guardrails para não virar um vetor de custo descontrolado em mãos de tenants sem restrições.
Objetivos e Não-Objetivos
Ficha Técnica
- Plataforma
- EKS (cenário composto, baseado em práticas reais)
- Tenants
- 30–60 times de produto
- Escala estimada
- 500–1500 pods em produção, picos de batch até 3x
- Região principal
- us-east-1 (multi-AZ: 3 AZs)
- Versão EKS
- 1.29+ (gerenciado, auto-upgrade via pipeline)
- Autoscaler
- Karpenter v1.x (substituindo Cluster Autoscaler)
- Custo / observabilidade
- Kubecost + AWS Cost Allocation Tags + Container Insights
- Identidade de workload
- EKS Pod Identity (preferido) + IRSA (legado)
- Rede
- VPC CNI + Network Policies (Calico ou VPC CNI nativo)
- Economia Spot estimada
- 50–70% vs On-Demand para workloads de batch (estimativa de mercado)
Design Proposto: Isolamento em Camadas
O modelo de isolamento adotado é namespace-as-tenant-boundary, com reforço em quatro camadas independentes. Nenhuma camada sozinha é suficiente; a defesa em profundidade é o princípio.
Camada 1 — Namespace e identidade: Cada tenant recebe um namespace dedicado (ou um conjunto deles, por ambiente: team-x-prod, team-x-staging). O provisionamento é automatizado via GitOps (ArgoCD ou Flux): um PR no repositório de plataforma cria o namespace, aplica ResourceQuota e LimitRange padrão, e cria o RoleBinding que concede ao ServiceAccount da equipe permissões de edit dentro do namespace, nunca cluster-admin. A política de RBAC segue o princípio de menor privilégio: equipes não conseguem criar ClusterRoles, acessar segredos de outros namespaces, ou modificar NetworkPolicies — essas operações são reservadas à equipe de plataforma.
Camada 2 — Rede: Uma NetworkPolicy default-deny-all é aplicada a cada namespace no momento da criação. Equipes precisam declarar explicitamente as políticas de ingress e egress que seus workloads necessitam. O tráfego entre namespaces é bloqueado por padrão; exceções são concedidas via política revisada pela equipe de plataforma. Isso elimina o risco de um tenant acessar inadvertidamente APIs internas de outro tenant. O DNS interno do cluster (kube-dns) permanece acessível para todos os namespaces, mas o acesso à API do Kubernetes a partir de pods é restrito via NetworkPolicy para o CIDR do control plane.
Camada 3 — Compute e quotas: ResourceQuota define limites de CPU, memória, número de pods, e contagem de objetos (Services, ConfigMaps, Secrets) por namespace. LimitRange define defaults de requests e limits para containers sem especificação explícita — isso é crítico para que o scheduler do Kubernetes e o Karpenter tomem decisões corretas de bin-packing. Workloads sem requests definidos são tratados como BestEffort e podem ser evicted primeiro sob pressão de memória. A equipe de plataforma define três tiers de quota (small, medium, large) e os tenants solicitam o tier adequado via PR.
Camada 4 — Identidade de workload: Nenhum pod deve carregar credenciais AWS estáticas. O modelo preferido é EKS Pod Identity, que associa um ServiceAccount Kubernetes a uma IAM Role via o agente eks-pod-identity-agent rodando como DaemonSet. A role é criada com escopo mínimo e revisada pelo time de segurança. IRSA permanece suportado para workloads legados, mas novos onboardings usam Pod Identity. Isso elimina a classe de vulnerabilidade onde credenciais são expostas em variáveis de ambiente ou ConfigMaps.
Arquitetura: EKS Multi-Tenant com Karpenter
Visão do cluster EKS mostrando separação de node pools por classe de workload, isolamento de namespace por tenant, fluxo de autoscaling via Karpenter e integração com serviços AWS de identidade e custo.
- Dev Teams · PR → Git
- ArgoCD / Flux · GitOps Controller
- EKS Control Plane · API Server / etcd
- IAM · Pod Identity / IRSA
- ECR · Container Registry
- Karpenter · NodePool Controller
- Kubecost · Cost Allocation
- Pod Identity Agent · DaemonSet
- OPA / Kyverno · Admission Webhook
- Namespace: team-a-prod · ResourceQuota + LimitRange
- Pods (team-a) · ServiceAccount → IAM Role A
- NetworkPolicy · default-deny + allow rules
- Namespace: team-b-prod · ResourceQuota + LimitRange
- Pods (team-b) · ServiceAccount → IAM Role B
- NetworkPolicy · default-deny + allow rules
- NodePool: system · On-Demand, m6i, tainted
- NodePool: production · On-Demand + Spot fallback · m6i / m7i / c6i
- NodePool: batch · Spot-first, diverse · r6i / m6i / c6i
- CloudWatch · Container Insights
- S3 + CUR · Cost & Usage Report
- AWS Cost Allocation Tags · team / env / service
Karpenter: NodePools, Spot e Consolidação
Karpenter substitui o Cluster Autoscaler com uma abordagem fundamentalmente diferente: em vez de escalar node groups pré-definidos, ele observa pods Unschedulable e provisiona o nó mais adequado diretamente via EC2 Fleet API, respeitando os constraints declarados no pod (nodeSelector, affinity, tolerations, topology spread). Isso permite bin-packing mais eficiente e latência de provisionamento significativamente menor.
Estrutura de NodePools: Defino três NodePools com responsabilidades distintas:
- system — nós On-Demand dedicados para workloads de plataforma (Karpenter, CoreDNS, Kubecost, admission webhooks). Esses nós têm um taint
CriticalAddonsOnlye são excluídos da consolidação agressiva. Uso instânciasm6i.largeoum6i.xlargefixas para previsibilidade.
- production — nós para workloads de produto com SLA. Estratégia: On-Demand como base, com capacidade Spot para até 30% dos nós quando disponível. O
EC2NodeClassdefine uma lista diversificada de tipos de instância (m6i,m7i,c6i,c7i) para maximizar disponibilidade de Spot.karpenter.sh/capacity-type: [on-demand, spot]comspot-to-on-demand-ratiocontrolado via weight. PodDisruptionBudgets são obrigatórios para workloads neste pool — sem PDB, o Karpenter pode consolidar nós e derrubar todos os pods de um Deployment de uma vez.
- batch — nós Spot-first para jobs de ML, ETL e CI. Aqui aceito interrupção com tolerância declarada (
karpenter.sh/interruption-queueintegrado ao SQS para receber notificações de interrupção EC2 e drenar nós graciosamente antes da terminação). A diversidade de tipos de instância é máxima neste pool —r6i,m6i,c6i,r7i,m7iem múltiplos tamanhos — para garantir que o Spot Placement Score seja alto.
Consolidação: Habilito consolidationPolicy: WhenUnderutilized para o pool de batch e WhenEmpty para produção. A diferença é importante: WhenUnderutilized permite ao Karpenter mover pods entre nós para liberar nós subutilizados (bin-packing ativo), enquanto WhenEmpty só remove nós completamente vazios. Para produção, a consolidação agressiva pode causar disruption excessiva; prefiro WhenEmpty com PDBs bem configurados.
Guardrails de custo: Sem restrições, um tenant pode criar um Job que solicita 1000 pods de 4 vCPU cada, e o Karpenter vai provisionar 250 nós c6i.xlarge em segundos. Para prevenir isso, ResourceQuota no namespace limita o total de CPU/memória requisitável. Adicionalmente, o NodePool define limits.cpu e limits.memory globais — o Karpenter não provisiona além desses limites, mesmo que haja pods pendentes. Isso cria um teto de custo por pool.
Alternativas Avaliadas
Cluster por Tenant (Hard Multi-Tenancy)
- Isolamento máximo: blast radius contido por cluster
- Upgrade e configuração independentes por tenant
- Adequado para requisitos de conformidade rígidos (PCI, HIPAA)
- Custo de controle plane: ~$0.10/h por cluster EKS = overhead significativo para muitos tenants
- Overhead operacional: upgrades, patches e monitoramento multiplicados por N clusters
- Fragmentação de capacidade: cada cluster precisa de nós mínimos para sistema
Reservado para tenants com requisitos de conformidade que o cluster compartilhado não pode satisfazer
Cluster Autoscaler (manter legado)
- Maduro e amplamente documentado
- Sem necessidade de migração
- Latência de scale-up: 2–5 min vs ~30s do Karpenter
- Node groups fixos: difícil diversificar tipos de instância para Spot
- Consolidação limitada: não realiza bin-packing ativo
- Overhead de configuração: dezenas de ASGs para cobrir diversidade de workloads
Rejeitado: o custo de oportunidade de não migrar para Karpenter é alto em escala
Namespace-as-Tenant (proposta)
- Eficiência de custo: bin-packing máximo, um único control plane
- Operação centralizada: upgrades, políticas e observabilidade unificados
- Onboarding rápido: GitOps automatiza criação de namespace em minutos
- Isolamento de kernel compartilhado: vulnerabilidades de container escape afetam todos os tenants
- Noisy neighbor em CPU/memória se quotas não forem bem calibradas
- Não adequado para conformidade PCI/HIPAA sem controles adicionais significativos
Adotado: melhor trade-off para o perfil de tenants descrito (equipes de produto sem requisitos de conformidade rígidos)
Service Mesh (Istio) para isolamento de rede
- mTLS automático entre serviços, observabilidade de tráfego L7
- Políticas de autorização mais expressivas que NetworkPolicy
- Overhead de sidecar: ~50–100MB RAM e ~0.5 vCPU por pod (estimativa)
- Complexidade operacional alta: curva de aprendizado, debugging de tráfego
- Latência adicional no data path
Adiado: NetworkPolicy cobre o modelo de ameaça atual; revisitar quando mTLS entre serviços for requisito explícito
Modelos de Alocação de Custo
| Modelo | Granularidade | Precisão | Complexidade Operacional | Adequado para Chargeback? | |
|---|---|---|---|---|---|
| Tags de Custo AWS (puro) | Conta / Serviço AWS | Baixa (não vê pod) | Baixa | Não — muito agregado | — |
| Kubecost (proposta) | Namespace / Label / Pod | Alta (modela CPU+RAM+net+storage) | Média (requer labels consistentes) | Sim — com labels corretos | — |
| OpenCost (alternativa OSS) | Namespace / Label | Média | Baixa (sem UI enterprise) | Parcialmente — requer integração custom | — |
| Split Cost Allocation (AWS nativo) | EKS namespace (via CUR) | Média (proporcional a requests) | Baixa (configuração no console) | Sim — integrado ao CUR/Cost Explorer | — |
Decisão: EKS Pod Identity sobre IRSA para novos workloads
IRSA (IAM Roles for Service Accounts) requer anotação no ServiceAccount e configuração de OIDC provider. EKS Pod Identity, lançado em 2023, simplifica o modelo: o agente no nó intercepta chamadas ao IMDS e injeta credenciais sem necessidade de anotação OIDC. Suporta múltiplas roles por ServiceAccount e não tem o limite de 100 OIDC providers por conta.
Novos workloads usam EKS Pod Identity. Workloads legados com IRSA são migrados oportunisticamente. O agente eks-pod-identity-agent é instalado como add-on gerenciado em todos os node groups.
- Simplificação do onboarding: equipes não precisam configurar OIDC trust policies manualmente
- Redução de risco: sem dependência de OIDC endpoint externo para autenticação de workload
- Período de coexistência IRSA + Pod Identity durante migração: documentar claramente qual mecanismo cada workload usa
Plano de Rollout
- 1
Fase 0 — Fundação (Semanas 1–2)
Instalar Karpenter via Helm no cluster existente (sem remover Cluster Autoscaler ainda). Criar NodePool
systemcom taints. Configurar EC2NodeClass com AMI family AL2023, subnets privadas e security groups corretos. Validar que workloads de plataforma (CoreDNS, Kubecost, admission webhooks) são agendados nos nós system. Instalar eks-pod-identity-agent como add-on gerenciado. - 2
Fase 1 — Migração de Autoscaling (Semanas 3–4)
Criar NodePools
productionebatch. Migrar 20% dos workloads de produção para nós gerenciados pelo Karpenter (via nodeSelector). Monitorar latência de provisionamento, taxa de Spot interruption e comportamento de consolidação. Após validação, escalar para 100% dos workloads e remover Cluster Autoscaler. Configurar fila SQS para interrupção Spot e validar drenagem graciosa. - 3
Fase 2 — Isolamento de Tenant (Semanas 5–7)
Implementar pipeline GitOps de onboarding de namespace: PR cria namespace, aplica ResourceQuota (tier selecionado), LimitRange, NetworkPolicy default-deny, RoleBinding. Instalar e configurar OPA Gatekeeper ou Kyverno com políticas de: (a) rejeitar pods sem resource requests, (b) rejeitar imagens sem digest ou de registries não aprovados, (c) rejeitar pods com
hostNetwork: trueouprivileged: truefora do namespace de plataforma. Auditar namespaces existentes e remediar violações. - 4
Fase 3 — Alocação de Custo (Semanas 8–10)
Configurar Kubecost com integração ao CUR (Cost & Usage Report) via S3. Definir taxonomia de labels obrigatórios:
team,env,service,cost-center. Implementar política Kyverno que rejeita Deployments sem os labels obrigatórios. Habilitar AWS Split Cost Allocation para EKS no Cost Explorer como validação cruzada. Publicar dashboard de showback por tenant. Definir processo de revisão mensal de custo com equipes. - 5
Fase 4 — Hardening e Observabilidade (Semanas 11–12)
Migrar workloads legados de IRSA para EKS Pod Identity. Habilitar EKS control plane logging (API, audit, authenticator) para CloudWatch. Configurar alertas de custo por namespace no Kubecost (threshold por tier de quota). Implementar runbook de resposta a Spot interruption. Realizar exercício de chaos: simular interrupção de nós Spot em produção e validar que PDBs previnem indisponibilidade. Documentar e publicar guia de onboarding para equipes.
Riscos e Mitigações
R1 — Consolidação Karpenter derruba workloads sem PDB: O Karpenter pode consolidar nós e evictar todos os pods de um Deployment de réplica única. Mitigação: política Kyverno que rejeita Deployments com replicas: 1 em produção sem PDB correspondente. Alertar equipes durante onboarding.
R2 — Spot interruption em pico de produção: Se o NodePool de produção tiver proporção Spot alta e o mercado EC2 estiver apertado, múltiplos nós podem ser interrompidos simultaneamente. Mitigação: limitar Spot a 30% do NodePool de produção; diversificar tipos de instância; configurar topologySpreadConstraints para distribuir pods entre AZs e tipos de nó.
R3 — Tenant escapa de namespace via RBAC misconfiguration: Um RoleBinding incorreto pode conceder acesso a recursos fora do namespace. Mitigação: auditoria periódica de RBAC com kubectl-who-can; política de admission que bloqueia criação de ClusterRoleBindings por ServiceAccounts de tenant.
R4 — Kubecost com dados imprecisos por labels inconsistentes: Workloads sem labels corretos aparecem como custo não alocado, invalidando o showback. Mitigação: política Kyverno obrigatória + período de grace de 30 dias para equipes existentes migrarem.
R5 — Versão EKS desatualizada aumenta superfície de ataque: CVEs em versões antigas do Kubernetes são exploráveis. Mitigação: pipeline de upgrade automatizado com janela de manutenção; política de não suportar versões com mais de 2 minor releases de distância da versão atual do EKS.
Avaliação Well-Architected
Segurança
Forte: RBAC com menor privilégio, NetworkPolicy default-deny, EKS Pod Identity sem credenciais estáticas, admission webhooks bloqueando configurações inseguras, audit logging habilitado.
Confiabilidade
Boa: PDBs obrigatórios, topologySpreadConstraints, Spot com fallback On-Demand, consolidação conservadora em produção. Risco residual: interrupção Spot simultânea em múltiplos nós.
Eficiência de performance
Alta: Karpenter provisiona nós em ~30s vs 2–5min do CAS; bin-packing ativo reduz fragmentação de recursos; LimitRange garante que todos os pods têm requests definidos para scheduling correto.
Sustentabilidade
Positivo: consolidação de nós reduz instâncias ociosas; Spot utiliza capacidade EC2 excedente; bin-packing eficiente reduz footprint total do cluster.
Métricas de Sucesso e Targets
- Latência de provisionamento de nó (p95)
- < 60 segundos (baseline CAS: ~3 minutos)
- Utilização média de CPU dos nós
- > 60% (baseline típico sem consolidação: 30–40%)
- % de custo alocado (Kubecost)
- > 90% (custo não alocado < 10%)
- Proporção Spot em workloads batch
- > 70% dos nós batch rodando em Spot
- Violações de NetworkPolicy bloqueadas
- 100% de namespaces com default-deny aplicado (auditado mensalmente)
- Tempo de onboarding de novo tenant
- < 30 minutos (PR aprovado → namespace pronto)
- Incidentes de cross-tenant data access
- 0 (medido via audit log do EKS)
- Redução de custo EC2 vs baseline (estimativa)
- 25–40% via consolidação + Spot (estimativa; depende do mix de workload)
O erro mais comum que vejo em plataformas multi-tenant Kubernetes não é técnico — é de sequenciamento. Equipes instalam Karpenter primeiro (porque é empolgante e os resultados são imediatos) e deixam isolamento e alocação de custo para depois. Seis meses depois, você tem um cluster eficiente que ninguém consegue auditar e onde um bug de quota pode causar um bill surpresa de cinco dígitos.
Minha recomendação é inverter a ordem de prioridade: fundação de isolamento primeiro, autoscaling depois, custo em paralelo. ResourceQuotas e NetworkPolicies são baratas de implementar e difíceis de retroagir — cada namespace sem default-deny é uma dívida técnica de segurança que cresce com o número de tenants.
Sobre Karpenter especificamente: a feature de consolidação é poderosa, mas precisa ser calibrada com cuidado em produção. Eu sempre começo com WhenEmpty e só movo para WhenUnderutilized depois de ter PDBs validados em todos os workloads críticos. A diferença entre os dois modos é a diferença entre 'remover nós vazios' e 'mover pods ativamente' — o segundo pode causar disruption inesperada se PDBs estiverem mal configurados ou ausentes.
Sobre custo: Kubecost é bom, mas a precisão dele depende inteiramente da disciplina de labels. Sem uma política de admission que force labels obrigatórios, você vai ter 30% do custo como 'unallocated' e nenhuma equipe vai confiar nos números. A política Kyverno de labels obrigatórios não é opcional — é o que torna o showback crível.
Por fim, sobre EKS Pod Identity vs IRSA: migre. O modelo é mais simples, mais seguro (sem OIDC thumbprint para gerenciar), e o agente trata da rotação de credenciais automaticamente. O único motivo para manter IRSA é compatibilidade com workloads que não conseguem ser migrados no curto prazo — e mesmo esses devem ter uma data de migração definida.
Veredicto
A arquitetura proposta é viável e representa o estado da arte para plataformas EKS multi-tenant em 2024–2025. A combinação de namespace-as-tenant-boundary com NetworkPolicy default-deny, ResourceQuota por tier, Karpenter com NodePools segregados e Kubecost com labels obrigatórios cobre os três vetores de falha identificados: isolamento insuficiente, custo invisível e autoscaling ineficiente. O design não é perfeito — nenhum cluster compartilhado é. O isolamento de kernel é o limite fundamental: um exploit de container escape afeta todos os tenants. Para organizações com requisitos de conformidade rígidos (PCI-DSS, HIPAA, tenants externos), cluster-per-tenant é a resposta correta, e o custo operacional adicional é justificado pelo modelo de ameaça. Para o perfil descrito — equipes de produto internas sem requisitos de conformidade rígidos — o trade-off de eficiência vs isolamento está bem calibrado. O risco de execução mais alto é o sequenciamento do rollout. A tentação de pular a Fase 2 (isolamento) para chegar logo na economia de custo da Fase 3 é real. Resista. Um incidente de cross-tenant causado por NetworkPolicy ausente ou RBAC mal configurado custa mais — em tempo de investigação, confiança das equipes e potencial impacto de segurança — do que qualquer economia de infraestrutura que você antecipou.
Post-mortems, ADRs e deep dives de arquitetura no seu email — do jeito que um arquiteto lê.
Sem spam · cancele quando quiser
Pergunte ao Fernando sobre isto
Receba uma resposta focada sobre este estudo do meu assistente de IA, baseada no meu trabalho.
Participe da conversa
Entre para comentar
Confirme seu e-mail para participar — você também recebe a newsletter. Sem senha.