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.
| Wave | Componentes |
|---|---|
| -1 | argocd-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) |
| 1 | cert-manager |
| 2 | acme-dns, storage, traefik, vault |
| 3 | crossplane, eso, cloudnativepg, redis-operator, argocd-ingress, traefik-ingress |
| 4 | crossplane-providers, eso-stores |
| 5 | crossplane-compositions (os 4 building blocks) |
| 6 | app-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)
| Pool | Label/Taint | Shapes | Capacity type | Consolidação |
|---|---|---|---|---|
data | platform.io/pool=data | A1.Flex (ARM) + E4.Flex (x86) | spot + on-demand | WhenEmpty, 10min |
workload | platform.io/pool=workload | E4.Flex + E5.Flex (x86) | spot + on-demand | WhenEmptyOrUnderutilized, 30s |
platform | sem 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, semcni.chainingMode— a combinação é inválida em 1.16+).routingMode: native,enableIPv4Masquerade: false(pods com IP real da VCN via NPN),kubeProxyReplacement: true. - Traefik —
LoadBalancervia OCI CCM, entrypointsweb→websecure(redirect permanente) ewebsecure. TLS 100% via cert-manager, Traefik só consome os Secrets. - cert-manager — 2
ClusterIssuer(letsencrypt-staging/letsencrypt-prod), HTTP-01 viaingressClassName: 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
ClusterSecretStorevault-backend, auth K8s (não token estático), KV v2.ExternalSecrets de bootstrap (deploy key protenants, credenciais do GitHub App do scaffolder) sempre precisam deNamespaceexplícito em sync-wave anterior —CreateNamespace=truesó cobre odestination.namespaceda 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).