Prophub Cloud Foundation

Arquitetura — plat-eng-platform

App-of-apps, os 4 building blocks Crossplane, node pools do Karpenter e os componentes de infraestrutura.

App-of-apps

O plat-eng-bootstrap aplica, uma única vez (Terraform, pós-cluster), um Application chamado platform apontando pra pasta apps/ deste repo. Esse é o único ponto de amarração entre os dois repos — daí em diante, tudo é GitOps: cada arquivo em apps/*.yaml é uma Application filha, aplicada na ordem definida por sync-wave.

WaveComponentes
-1argocd-projects (os 3 AppProject: core, networking, app-delivery)
0 (implícito)cilium, karpenter — sem sync-wave explícito, caem na wave 0 por padrão do ArgoCD (karpenter deveria ter wave própria; conhecido, não corrigido)
1cert-manager
2acme-dns, storage, traefik, vault
3crossplane, eso, cloudnativepg, redis-operator, argocd-ingress, traefik-ingress
4crossplane-providers, eso-stores
5crossplane-compositions (os 4 building blocks)
6app-delivery (o ApplicationSet)

karpenter está pinado com targetRevision: ">=1.1.0" — floating, não uma versão exata. Conhecido como anti-padrão GitOps, ainda não corrigido.

O acme-dns (wave 2) é o único componente cuja razão de existir não é óbvia pelo manifesto: ele emite certificado para domínio próprio de cliente, hospedado em DNS que a Prophub não administra — o caso que o solver Cloudflare não cobre. Onboarding, troubleshooting e riscos em aberto estão em docs/runbooks/acme-dns.mdx, no repo (runbooks não publicam neste site — ver o --exclude em .github/workflows/docs-sync.yml).

Os 4 building blocks Crossplane

Todos usam provider-kubernetes (Object genérico), não providers nativos OCI — cada Composition monta um manifesto Kubernetes real via function-patch-and-transform.

TenantNamespace

spec vazio de propósito — "modelo único, sem tier/plan; a identidade do tenant é o próprio nome". Provisiona um Namespace + uma CiliumNetworkPolicy (<nome>-deny-cross-ns) que nega ingress cross-namespace exceto: mesmo namespace, kube-system, cnpg-system, ot-operators, traefik (sem essa última, ingress externo dá 502 — parece bug de app, é rede).

PostgresInstance

spec.tenant obrigatório; instances (default 2), cpu (default 500m, só request, sem limit), memory (default 4Gi), storageGB (default 20). Provisiona um Cluster CNPG, storageClass: oci-bv-data, primaryUpdateStrategy: unsupervised. Afinidade obrigatória (required, não preferred) no pool data — um preferred já deixou um Redis preso pra sempre no pool platform sem taint.

RedisCache

spec.tenant obrigatório; cpu (default 100m), memory (default 512Mi). Sem campo clusterSize — removido 2026-07-31 porque o Redis do OpsTree é sempre standalone e o campo era descartado em silêncio. Afinidade obrigatória em data + preferência (peso 100) por on-demand.

RedisPersistent

spec.tenant obrigatório; cpu (default 2, com limit igual ao request — única exceção à convenção CPU-request-only do resto da plataforma), memory (default 512Mi), storageGB (default 2), e um clusterSize que existe no schema mas não tem efeito nenhum (mesma limitação standalone do OpsTree — mantido porque o argumento de HA pra estado persistente continua válido, só não implementado ainda). PVC em oci-bv-data, initContainer faz chown 1000:1000 /data.

Node pools (Karpenter)

PoolLabel/TaintShapesCapacity typeConsolidação
dataplatform.io/pool=dataA1.Flex (ARM) + E4.Flex (x86)spot + on-demandWhenEmpty, 10min
workloadplatform.io/pool=workloadE4.Flex + E5.Flex (x86)spot + on-demandWhenEmptyOrUnderutilized, 30s
platformsem taint— (estático, não-Karpenter)——

platform não é NodePool do Karpenter — são os 2 nós ARM A1.Flex estáticos criados pelo Terraform do bootstrap, sem taint. É o único pool sem proteção — causa raiz recorrente de workload "caindo" ali por acidente quando falta nodeSelector/toleration explícito.

Componentes de infraestrutura

  • Cilium — chaining sobre o CNI nativo da OCI (cni.chainingTarget: oci, sem cni.chainingMode — a combinação é inválida em 1.16+). routingMode: native, enableIPv4Masquerade: false (pods com IP real da VCN via NPN), kubeProxyReplacement: true.
  • Traefik — LoadBalancer via OCI CCM, entrypoints web→websecure (redirect permanente) e websecure. TLS 100% via cert-manager, Traefik só consome os Secrets.
  • cert-manager — 2 ClusterIssuer (letsencrypt-staging/letsencrypt-prod), HTTP-01 via ingressClassName: traefik.
  • Vault — modo persistente, não dev (storage "file", PVC 50Gi). Auto-unseal via OCI KMS (seal "ocikms"), autenticando como instance principal do nó — único componente que faz isso. ha.enabled: false (single pod; a pendência pedia persistência, não HA).
  • ESO — backend Vault via ClusterSecretStore vault-backend, auth K8s (não token estático), KV v2. ExternalSecrets de bootstrap (deploy key pro tenants, credenciais do GitHub App do scaffolder) sempre precisam de Namespace explícito em sync-wave anterior — CreateNamespace=true só cobre o destination.namespace da própria Application.

Blueprints e app-delivery

O contrato de entrega de app (app.yaml → app-delivery.yaml+values.yaml → chart prophub-{static,web,worker,job} → ApplicationSet) e os Blueprints (app-delivery/blueprints/) têm documentação própria e detalhada em app-delivery/README.md — este repo não duplica aqui. Resumo: 4 formas de app, cada uma com seu chart; um Blueprint (hoje só prophub-tenant) compõe múltiplos recursos (claims + apps) num template único que o scaffolder instancia via scaffold stack.

Validação em CI

ci/validate.py (workflow Validate & Build) roda 3 checks antes de qualquer merge: pool (todo workload com afinidade/toleration correta pro pool certo), fields (campos de CRD que o Kubernetes descartaria em silêncio), xrd (coerência XRD↔Composition). Nasceu de uma auditoria manual que achou 8 bugs reais de plataforma, nenhum detectado como erro na hora — o Crossplane não valida o que entrega, e o API server poda campos desconhecidos sem avisar. Não cobre nada que exija API server ao vivo (admission webhooks, dry-run) — os runners do GitHub não alcançam o endpoint do OKE (admin_cidrs restringe).

On this page