NomadPaaS — Architecture

Multicloud PaaS · Fiber · Nomad · Consul · Traefik · OAM
Go + Fiber v2 Nomad v1.7 Consul v1.17 Traefik v3 OAM v0.3
Full System Architecture
▸ END-TO-END TRAFFIC & CONTROL FLOW
graph TB %% ── External ────────────────────────────────────────────── Browser["🌐 Browser / CLI\nTenant User"]:::ext Admin["🔑 Platform Admin\nAPI Key / OIDC"]:::ext %% ── Edge layer ──────────────────────────────────────────── subgraph EDGE[" ⚡ EDGE LAYER "] Traefik["⬡ Traefik v3\nL7 Reverse Proxy\n+ TLS Termination\n+ Rate Limiting"]:::traefik end %% ── Control plane ───────────────────────────────────────── subgraph CP[" 🧠 CONTROL PLANE (Go + Fiber v2) "] direction TB FiberAPI["▶ Fiber HTTP Server\nMiddleware Stack\nrecovery · cors · limiter\nidempotency · zap"]:::fiber subgraph HANDLERS["API Handlers /api/v1"] H_Tenant["Tenant\nHandler"]:::handler H_App["App\nHandler"]:::handler H_Cluster["Cluster\nHandler"]:::handler H_Events["Events\nHandler"]:::handler end subgraph CORE["Core Services"] OAMTranslator["📐 OAM Translator\nApplication → Nomad Job\nComponent → TaskGroup\nTrait → Constraint/Tag"]:::core Store["💾 State Store\nTenants · Apps · Events\n(memory | etcd | pg)"]:::core Metrics["📊 Prometheus\nMetrics Registry\nHTTP · Deploy · Nomad"]:::core end FiberAPI --> H_Tenant & H_App & H_Cluster & H_Events H_App --> OAMTranslator H_Tenant & H_App --> Store FiberAPI --> Metrics end %% ── HashiCorp stack ─────────────────────────────────────── subgraph HASHI[" 🔶 HASHICORP STACK "] direction LR subgraph NomadMgr["Nomad Cluster Manager"] Strategy["Scheduling Strategy\nprimary · round-robin\nleast-loaded"]:::nomad end subgraph ConsulMgr["Consul Tenant Manager"] ConsulNS["Namespace\nIsolation"]:::consul ConsulACL["ACL Policies\n+ Tokens"]:::consul ConsulIntentions["Service Mesh\nIntentions"]:::consul end subgraph TraefikWriter["Traefik Config Writer"] DynYAML["YAML Config\nWriter (atomic\nrename)"]:::traefik2 end Vault["🔐 Vault\nSecret Engine\nPKI · KV · Dynamic"]:::vault end %% ── Multicloud Nomad clusters ───────────────────────────── subgraph CLOUDS[" ☁ MULTICLOUD NOMAD CLUSTERS "] direction LR subgraph AWS["▲ AWS"] NomadAWS["Nomad Cluster\nus-east-1"]:::aws ConsulAWS["Consul\nDatacenter"]:::consul end subgraph GCP["◆ GCP"] NomadGCP["Nomad Cluster\neurope-west1"]:::gcp ConsulGCP["Consul\nDatacenter"]:::consul end subgraph AZURE["■ Azure"] NomadAzure["Nomad Cluster\neastasia"]:::azure ConsulAzure["Consul\nDatacenter"]:::consul end end %% ── Workloads ───────────────────────────────────────────── subgraph WORKLOADS[" 📦 TENANT WORKLOADS "] direction LR WL1["WebService\n(service job)"]:::wl WL2["Worker\n(service job)"]:::wl WL3["CronTask\n(periodic batch)"]:::wl WL4["Daemon\n(system job)"]:::wl end %% ── UI ──────────────────────────────────────────────────── UI["⬕ Svelte UI\n+ Tailwind CSS\nSPA Dashboard"]:::ui %% ── Edges ───────────────────────────────────────────────── Browser -->|"HTTPS"| Traefik Admin -->|"X-API-Key\nor OIDC JWT"| Traefik UI -->|"REST /api/v1"| Traefik Traefik -->|"Consul catalog\nservice discovery"| ConsulMgr Traefik -->|"File provider\nwatches dir"| DynYAML Traefik -->|"Route to tenant\nworkload"| WORKLOADS FiberAPI -->|"auth middleware\nrequest routing"| HANDLERS H_Tenant -->|"provision/deprovision"| ConsulMgr H_Tenant -->|"write routing rules"| TraefikWriter H_App -->|"submit / deregister\nnomad jobs"| NomadMgr H_Cluster -->|"query nodes,\njobs, allocs"| NomadMgr OAMTranslator -->|"nomadapi.Job"| NomadMgr NomadMgr -->|"strategy: primary\nround-robin\nleast-loaded"| Strategy Strategy -->|"Jobs().Register()"| NomadAWS & NomadGCP & NomadAzure ConsulMgr --> ConsulNS & ConsulACL & ConsulIntentions ConsulNS & ConsulACL -->|"namespace ACLs"| ConsulAWS & ConsulGCP & ConsulAzure NomadAWS & NomadGCP & NomadAzure -->|"run allocs"| WORKLOADS WORKLOADS -->|"register services"| ConsulAWS & ConsulGCP & ConsulAzure WORKLOADS -->|"fetch secrets"| Vault classDef ext fill:#0d1520,stroke:#1e3a5a,color:#7ab8f5 classDef traefik fill:#0a1e28,stroke:#26b5d8,color:#26b5d8 classDef traefik2 fill:#0a1e28,stroke:#26b5d866,color:#26b5d8 classDef fiber fill:#0a200e,stroke:#00de64,color:#00de64 classDef handler fill:#071a0b,stroke:#1a4025,color:#6ed89a classDef core fill:#071a0b,stroke:#2a5535,color:#8fc4a0 classDef nomad fill:#0d1a06,stroke:#5da832,color:#7dd14d classDef consul fill:#1a0f06,stroke:#e76d3b,color:#e76d3b classDef vault fill:#1a1006,stroke:#c8a84b,color:#c8a84b classDef aws fill:#1a0e00,stroke:#ff9900,color:#ff9900 classDef gcp fill:#060e1a,stroke:#4285f4,color:#4285f4 classDef azure fill:#00091a,stroke:#0078d4,color:#0078d4 classDef wl fill:#0d1a12,stroke:#2a4a30,color:#6ea880 classDef ui fill:#140a1a,stroke:#8b5cf6,color:#a78bfa
OAM → Nomad Translation Pipeline
▸ OPEN APPLICATION MODEL RESOURCE FLOW
flowchart LR subgraph INPUT[" OAM Input (JSON/YAML) "] direction TB APP["Application\napiVersion: core.oam.dev/v1beta1\nkind: Application"]:::oam COMP["ApplicationComponent\n• name, type (webservice|worker…)\n• properties (image, ports, env)\n• targetClusters"]:::oam TRAITS["Traits\n• scaler → replicas, min/max\n• ingress → host, pathPrefix, TLS\n• vault-secret → path, env map\n• affinity → cloud, region, nodeClass\n• volume → type, source, mountPath"]:::oam APP --> COMP --> TRAITS end subgraph TRANS[" Translator (Go) "] direction TB T1["componentKindToNomadType()\nwebservice/worker → service\ntask/cron-task → batch\ndaemon → system"]:::trans T2["buildTaskGroup()\ncount from ScalerTrait\nRestartPolicy, UpdateStrategy\nMigrateStrategy"]:::trans T3["buildTraefikTags()\nHost() / PathPrefix() rules\ncertresolver tags\nsticky cookie, healthcheck"]:::trans T4["buildConstraints()\n${meta.cloud}\n${meta.cloud_region}\n${node.class}"]:::trans T5["buildVaultTemplate()\nconsul-template syntax\nenvvars = true"]:::trans T6["buildScalingPolicy()\nNomad Autoscaler\ntarget-value strategy"]:::trans T1 --> T2 T2 --> T3 & T4 & T5 & T6 end subgraph OUTPUT[" Nomad Job Spec "] direction TB JOB["nomadapi.Job\n• ID, Namespace, Type\n• Datacenters, Meta\n• Periodic (cron)\n• Vault policy"]:::nomad2 TG["TaskGroup\n• Count (replicas)\n• RestartPolicy\n• UpdateStrategy\n• Spread stanza\n• Constraints"]:::nomad2 SVC["Service\n• Provider: consul\n• Traefik tags\n• Connect sidecar\n• Health checks"]:::nomad2 TASK["Task (Docker)\n• image, ports\n• Env map\n• Resources\n• Templates (Vault)\n• VolumeMounts"]:::nomad2 SCALE["ScalingPolicy\n• min/max\n• nomad-apm source\n• target-value\n• cooldown: 2m"]:::nomad2 JOB --> TG --> SVC & TASK & SCALE end INPUT -->|"Translator.Translate()\napp, tenantID"| TRANS TRANS -->|"*nomadapi.Job\nper component"| OUTPUT OUTPUT -->|"Jobs().Register()"| NOMAD[("Nomad\nCluster")]:::cluster classDef oam fill:#0e0a1a,stroke:#a78bfa,color:#c4aafa classDef trans fill:#071a0b,stroke:#2a5535,color:#7aad88 classDef nomad2 fill:#0d1a06,stroke:#5da832,color:#7dd14d classDef cluster fill:#0d1a06,stroke:#5da832,color:#7dd14d
Multi-Tenant Isolation Model
▸ TENANT PROVISIONING & ISOLATION BOUNDARIES
sequenceDiagram actor Admin participant Fiber as Fiber API participant Store as State Store participant Consul as Consul Manager participant Nomad as Nomad Manager participant Traefik as Traefik Writer participant Vault as Vault Admin->>+Fiber: POST /api/v1/tenants\n{name, domain, quotas} Fiber->>Store: CreateTenant() → UUID Store-->>Fiber: *Tenant{id, name, ...} Fiber->>+Consul: ProvisionTenant(tenantID) Consul->>Consul: ensureNamespace("tenant-{id}") Consul->>Consul: ensurePolicy(ns) → ACL HCL rules Consul->>Consul: ensureToken(policyID) → SecretID Consul->>Consul: ensureIntentions()\nAllow intra-ns\nDeny cross-ns Consul-->>-Fiber: TenantResources{namespace, token} loop Each Nomad cluster Fiber->>Nomad: EnsureNamespace("tenant-{id}") Nomad-->>Fiber: OK (idempotent) end Fiber->>Traefik: WriteTenantConfig(tenantID, domain) Note over Traefik: Writes tenant-{id}.yml atomically\n• Rate limit middleware\n• Tenant header injection\n• Security headers chain\n• Domain catch-all router Fiber->>Store: AddEvent("tenant.create") Fiber-->>-Admin: 201 Created {tenant} Note over Admin,Vault: Later: App Deploy Admin->>+Fiber: POST /tenants/{id}/apps\nOAM Application JSON Fiber->>Fiber: OAMTranslator.Translate(app, tenantID) Note over Fiber: Per component:\n→ Nomad Job\n→ Traefik tags in Consul service\n→ Vault template blocks loop Each component Fiber->>+Nomad: SubmitJob(cluster, *nomadapi.Job) Nomad->>Nomad: Pick cluster\n(strategy: primary | round-robin | least-loaded) Nomad-->>-Fiber: EvalID end Fiber->>Store: SaveApp(tenantID, app) Fiber-->>-Admin: 202 Accepted {app, submitted[]}
Live Request Routing (Traefik + Consul)
▸ L7 TRAFFIC PATH FOR TENANT WORKLOADS
flowchart TD REQ["HTTPS Request\nHost: api.tenant-a.acme.com"]:::req subgraph TRAEFIK["Traefik v3"] EP["EntryPoint: websecure\n:443"]:::t TLS["TLS Termination\nLet's Encrypt ACME\nor internal Vault PKI"]:::t DISCOVERY["Consul Catalog\nProvider\n• polls every 15s\n• reads Traefik tags\n• builds route table"]:::t subgraph MW["Middleware Chain (per tenant)"] RL["Rate Limiter\n100 req/min avg\nburst 50"]:::mw TH["Tenant Headers\nX-Tenant-ID\nX-Tenant-Namespace\nX-Forwarded-By"]:::mw SEC["Security Headers\nHSTS · X-Frame\nContent-Type\nReferrer-Policy"]:::mw CB["Circuit Breaker\nNetworkError > 30%\nor 5xx > 25%"]:::mw end ROUTER["Router Match\nHost(api.tenant-a.acme.com)\npriority: auto"]:::t LB["Load Balancer\nRound Robin\nsticky cookie\nhealthcheck: /healthz 10s"]:::t end subgraph CONSUL["Consul Service Mesh"] CAT["Catalog\nservice: api-api-http\ntags: [traefik.enable=true, …]"]:::consul ENVOY1["Envoy Sidecar\nmTLS upstream"]:::consul end subgraph NOMAD["Nomad Allocations (namespace: tenant-abc)"] ALLOC1["Alloc 1\nDocker: api:v2.1\nport: 8080"]:::alloc ALLOC2["Alloc 2\nDocker: api:v2.1\nport: 8080"]:::alloc ALLOC3["Alloc 3\nDocker: api:v2.1\nport: 8080"]:::alloc end REQ --> EP --> TLS --> DISCOVERY DISCOVERY <-->|"register/watch"| CAT TLS --> ROUTER --> MW RL --> TH --> SEC --> CB --> LB LB -->|"upstream: consul\ncatalog service"| ENVOY1 ENVOY1 -->|"mTLS"| ALLOC1 & ALLOC2 & ALLOC3 CAT -.->|"Traefik tag\nparsing"| ROUTER classDef req fill:#0d1520,stroke:#1e3a5a,color:#7ab8f5 classDef t fill:#0a1e28,stroke:#26b5d8,color:#26b5d8 classDef mw fill:#081620,stroke:#26b5d855,color:#7acde8 classDef consul fill:#1a0f06,stroke:#e76d3b,color:#e76d3b classDef alloc fill:#0d1a06,stroke:#5da832,color:#7dd14d
Component Reference
Control Plane
Fiber HTTP Server
Go + Fiber v2. Handles all REST API traffic. Middleware: recovery, CORS, zap logging, rate limiter, idempotency keys, Prometheus instrumentation.
cmd/server/main.go
internal/api/handler.go
Fiber v2 validator
Workload Schema
OAM Translator
Converts OAM Application resources into Nomad API Job structs. Decodes typed traits (scaler, ingress, vault-secret, affinity, volume) via generic JSON round-trip.
internal/oam/translator.go
internal/oam/types.go
OAM v0.3 nomadapi
Orchestration
Nomad Manager
Manages N Nomad cluster clients. Scheduling strategies: primary, round-robin, least-loaded. Concurrent job listing via goroutines. Namespace provisioning.
internal/nomad/client.go
multi-cluster autoscale
Service Mesh
Consul Manager
Provisions per-tenant Consul namespaces, ACL policies, ACL tokens, and Connect service mesh intentions. Allow intra-tenant, deny cross-tenant by default.
internal/consul/tenant.go
namespace intentions
Ingress / Proxy
Traefik Writer
Atomically writes YAML dynamic config files into Traefik's watched directory. Per-tenant middleware chains, rate limiting, security headers, TLS cert resolvers.
internal/traefik/config.go
atomic write file provider
Secrets
Vault Integration
Dynamic secret injection via Nomad template blocks using consul-template syntax. Per-tenant Vault policies. PKI for internal mTLS. Change mode: restart or signal.
internal/oam/translator.go\nbuildVaultTemplate()
dynamic PKI
State
Store Layer
In-memory store with RWMutex for thread safety. Tenants, apps, events. Pluggable backend — swap for etcd or PostgreSQL without changing handler code.
internal/store/store.go
memory etcd-ready
Observability
Metrics & Logging
Prometheus metrics: HTTP request count/duration, deployment rate/latency, Nomad submit errors, tenant/app gauges. Zap structured logging. pprof in debug mode.
internal/metrics/metrics.go
internal/middleware/middleware.go
prometheus zap

Deploy Flow

Tenant POSTs OAM Application JSON → Fiber validates + parses → OAM Translator converts each ApplicationComponent into a nomadapi.Job → Nomad Manager picks cluster per scheduling strategy → Jobs().Register() called per component → Consul service tags auto-configure Traefik routing.

Scheduling Strategies

primary — all jobs to designated primary cluster.
round-robin — atomic counter cycles across clusters.
least-loaded — polls allocation count per cluster, picks minimum.
Override per-component via targetClusters: [aws-us-east].

Tenant Isolation

Each tenant maps to a Nomad namespace (quota enforcement), a Consul namespace (service mesh ACL), a Vault namespace (secret isolation), and a Traefik middleware chain (rate limit + header injection + security hardening).