Skip to content

Kubernetes

Audience

PLANA staff working on infrastructure. Customers do not need this page.

PLANA runs on a single Kubernetes cluster operated by Exoscale SKS (Scalable Kubernetes Service). One cluster across all environments — we do not run a separate staging cluster. Isolation between environments is by namespace, not by cluster.

The cluster

PropertyValue
Nameplana-pulse-bg-sof-1
ProviderExoscale SKS
Regionbg-sof-1 (Sofia, Bulgaria)
Kubernetes version1.35.3
Nodes3
Container runtimecontainerd
CNICalico
Load balancerExoscale NLB plana-pulse-eg-lb at 194.182.177.67
kubeconfig~/.kube/config (default context)
Exoscale orgb51076cb-0409-45a9-8acf-d1153a7e86e9

The control plane is managed by Exoscale; we operate only the data plane (worker nodes) and the workloads on top.

Namespaces

A namespace is a per-concern boundary. Tenant data lives in plana-odoo[-{17,18,19}]; everything else is a per-product or per-tool namespace.

Tenant namespaces

NamespacePurposeNotes
plana-odoov17 tenant workers + the shared eg-gatewayLegacy version; will be emptied as v17 tenants migrate. The eg-gateway Gateway resource lives here permanently.
plana-odoo-18v18 tenant workers — current defaultWhere new tenants land
plana-odoo-19v19 tenant workers — limited releaseEmpty Deployment scaled to 0 until first tenant lands

Each version namespace has its own worker-odoo Deployment with HPA, its own filestore PVC, and its own quota.

Product namespaces

NamespacePurpose
pulse-accountpulse-account (Nuxt), pulse-account-api, bos-portal (prod)
bos-demoBOS development / preview
authentikAuthentik server + worker — PLANA staff SSO
pulse-eventsRedis Streams event bus consumer + producer wrappers
crossplane-systemCrossplane reconciler + per-tenant side-effect Jobs
cert-managerLet's Encrypt issuance
envoy-gateway-systemEnvoy Gateway control + data plane
crowdsecLAPI, agents, bouncer (ExtAuthz target for Envoy)
monitoringPrometheus, Grafana, Alertmanager, Loki, Blackbox Exporter
falcoFalco — deprecated, kept disabled (see Security → Runtime security)
backupPostgreSQL backup CronJobs (one per tenant)
ai-bos-agentMulti-tenant BOS agent runtime
ai-marketing-agentMulti-tenant marketing agent runtime

Tools namespaces

NamespacePurpose
forgejoGit server (git.planapulse.com)
forgejo-runnerCI runners for the main org
matrixSynapse, Element Web, hookshot
nextcloudNextcloud + Collabora + Talk
penpotPenpot (self-hosted Figma)
matomoMatomo 5 + MariaDB 11
vaultwardenVaultwarden (Authentik-only SSO)
pgadminpgAdmin for ad-hoc DB inspection
openphysicaExternal client project on shared infra

Customer cluster operations

Customer-visible operations all happen by applying CRs in crossplane-system. There is no operator-only namespace customers can see.

Worker nodes

Three Exoscale instances, each Medium (4 vCPU, 8 GiB) or larger depending on load. Autoscaling is handled at the cluster level — nodes can be added when the scheduler pressures, but the default is a fixed pool of three.

Each tenant Odoo worker runs as a normal Deployment with HorizontalPodAutoscaler on CPU. We do not pin tenants to nodes — pods are scheduled wherever there is capacity. Anti-affinity rules ensure HPA replicas of the same Deployment land on different nodes.

Resource quotas

Every product namespace has a ResourceQuota to prevent one tenant or service from consuming the entire cluster:

NamespaceCPU limitMemory limitPod cap
plana-odoo1014 GiB14
plana-odoo-181014 GiB14
plana-odoo-1946 GiB8
pulse-account46 GiB8
ai-bos-agent23 GiB4

Quotas are intentionally tight. When CI bumps them — for example when running a TemplateSnapshot Job alongside HPA worker replicas — we re-size afterwards rather than leaving headroom open.

Storage classes

ClassProviderUse
exoscale-bs-retainExoscale block storage (retained on PVC delete)Tool databases (Matomo MariaDB, Nextcloud, Forgejo, Vaultwarden, etc.)
exoscale-bs-deleteExoscale block storage (deleted with PVC)Ephemeral build caches
(NFS, via PV)nfs01.planapulse.comPer-tenant Odoo filestore

The NFS filestore is mounted into each Odoo worker pod under /var/lib/odoo/filestore/<database-name>. The mount is sub-path-scoped per tenant, so a worker pod for tenant A cannot read tenant B's files even though both are on the same NFS export.

kubectl and Flux

Day-to-day cluster work is split between two access patterns:

  • kubectl — for ad-hoc reads, debugging, and one-off Jobs.
  • Flux GitOps — for everything declared in infra/k8s/. Flux reconciles the main branch of plana-pulse/infra into the cluster on a short interval. Manifests merged to main are deployed without human intervention.

A strict rule: never kubectl apply a manifest that is also managed by Flux. The reconciler will revert your change within seconds, and you risk clobbering an in-flight Flux change at the same time. Edits go through Pull Request → Flux reconcile. See Flux GitOps for the procedure.

CRI, networking, and DNS

  • Container runtime: containerd (Exoscale default).
  • CNI: Calico, with NetworkPolicy enforcement enabled.
  • DNS: CoreDNS in kube-system. Pod DNS resolves *.svc.cluster.local and an internal *.planapulse.com zone via the Exoscale resolver.
  • Hairpin NAT: the Exoscale NLB does not hairpin — a pod inside the cluster cannot reach a *.planapulse.com hostname over the public LB IP. We work around this in two patterns: a hostAlias inside the pod pointing to the Envoy ClusterIP, or in-cluster Service URLs everywhere. See Shared infrastructure → Mailu for an example.

Where to read more

© PLANA Digital Ltd.