Skip to content

Operations

All runtime binaries read configuration from environment variables via sdk/config.MustBind. Variables are injected by Helm ConfigMap → Deployment env block.

VariableDefaultDescription
GRPC_PORT50051gRPC listen port (server + tenant pods).
LOG_LEVELinfoLog verbosity. One of: debug, info, warn, error.
OTEL_EXPORTER_OTLP_ENDPOINT(empty)OTLP endpoint for traces/metrics. Empty disables export.
ENVdevDeployment environment. One of: dev, staging, prod.
VariableDefaultDescription
IDENTITY_GRPC_ADDRidentity.paper-board.svc.cluster.local:50051gRPC target for identity.AuthService (used to verify tenant existence).
TENANT_POD_IMAGEghcr.io/paper-board/runtime-server:v0.1.0Container image for per-tenant runtime Deployments.
TENANT_POD_CPU_REQUEST100mCPU request for tenant pods.
TENANT_POD_CPU_LIMIT500mCPU limit for tenant pods.
TENANT_POD_MEMORY_REQUEST128MiMemory request for tenant pods.
TENANT_POD_MEMORY_LIMIT512MiMemory limit for tenant pods.
K8S_NAMESPACEpaper-boardKubernetes namespace where tenant Deployments live.
K8S_DNS_SUFFIXpaper-board.svc.cluster.localCluster DNS suffix appended to tenant Service names.
IDLE_TIMEOUT_MINUTES(empty)Override idle timeout in minutes. Empty = 60 minutes. Intended for tests.
VariableDefaultDescription
ROUTER_HTTP_PORT8080HTTP listen port for the router.
IDENTITY_GRPC_ADDRidentity.paper-board.svc.cluster.local:50051gRPC target for JWT verification.
TENANT_GRPC_PORT50051gRPC port on tenant pods that the router dials.
K8S_NAMESPACEpaper-boardNamespace to look up tenant Deployments in.
K8S_DNS_SUFFIXpaper-board.svc.cluster.localDNS suffix used when building per-tenant gRPC targets.

Chart: helm/runtime/ — published as oci://ghcr.io/paper-board/charts/runtime.

server:
enabled: true
replicas: 1
image:
repository: ghcr.io/paper-board/runtime-server
tag: v0.1.0
pullPolicy: IfNotPresent
service:
type: ClusterIP
port: 50051
config:
grpcPort: 50051
logLevel: "info"
otlpEndpoint: ""
env: "dev"
resources:
requests: { cpu: 100m, memory: 128Mi }
limits: { cpu: 500m, memory: 256Mi }

server.enabled controls the base server Deployment. This is separate from per-tenant Deployments, which the controller manages at runtime (not via Helm).

controller:
enabled: true
replicas: 1
image:
repository: ghcr.io/paper-board/runtime-controller
tag: v0.1.0
pullPolicy: IfNotPresent
config:
logLevel: "info"
env: "dev"
identityGRPCAddr: "identity.paper-board.svc.cluster.local:50051"
resources:
requests: { cpu: 50m, memory: 64Mi }
limits: { cpu: 200m, memory: 256Mi }

The controller runs as a single replica. It does not need HA at Phase 3 — losing the controller for a short period only delays idle sweep; EnsureReady calls from the router will restart it cleanly on recovery.

router:
enabled: true
replicas: 3
image:
repository: ghcr.io/paper-board/runtime-router
tag: v0.1.0
pullPolicy: IfNotPresent
config:
httpPort: 8080
logLevel: "info"
env: "dev"
identityGRPCAddr: "identity.paper-board.svc.cluster.local:50051"
k8sDNSSuffix: "paper-board.svc.cluster.local"
tenantGRPCPort: 50051
resources:
requests: { cpu: 50m, memory: 64Mi }
limits: { cpu: 200m, memory: 256Mi }

Three router replicas by default. The router is stateless; any replica can handle any request.

tenantPod:
image:
repository: ghcr.io/paper-board/runtime-server
tag: v0.1.0
pullPolicy: IfNotPresent
resources:
requests: { cpu: 100m, memory: 128Mi }
limits: { cpu: 500m, memory: 512Mi }
idleTimeoutSeconds: 3600

tenantPod values are read by the controller at startup and injected into TenantPodConfig. They are not applied by Helm directly to tenant Deployments — the controller applies them when calling EnsureReady for each tenant.

migrator:
enabled: false

Runtime has no Postgres schema (ADR-0002). The migrator Job is disabled. The enabled: false block is retained for service-template structural parity only.


The controller runs as a single-replica Deployment in the paper-board namespace. It requires a ClusterRole with the following permissions:

ResourceVerbs
deployments (apps/v1)get, list, create, update, patch, delete
services (core/v1)get, list, create, update, patch, delete

These are granted by the runtime-controller ClusterRole in helm/runtime/templates/controller-deployment.yaml. The controller’s ServiceAccount is bound to this role via ClusterRoleBinding in the same template.

Sweep schedule

SweepFrequencyTrigger
Idle sweepEvery 5 minutesInternal ticker in RunSweepLoop
Weekly sweepEvery 24 hoursSecond ticker in RunSweepLoop

The controller blocks on RunSweepLoop until context cancellation. Graceful shutdown waits for the current sweep iteration to finish.


Deployments and Services for tenant pods follow a deterministic naming scheme:

Deployment name: runtime-t-{tenant_short}
Service name: runtime-t-{tenant_short}
DNS target: runtime-t-{tenant_short}.paper-board.svc.cluster.local:50051

tenant_short is derived by the controller as lower(hex(tenant_uuid)[0:8]). On collision (two tenants sharing the same 8-character hex prefix), the controller appends -2, -3, and so on, storing the resolved value in the Deployment annotation runtime.paper-board.io/tenant-short. Subsequent lookups for the same tenant return the stored annotation rather than recomputing.

To list all tenant Deployments:

Terminal window
kubectl get deployments -n paper-board -l runtime.paper-board.io/tenant=true

To find which tenant a Deployment belongs to:

Terminal window
kubectl get deployment -n paper-board runtime-t-<short> \
-o jsonpath='{.metadata.annotations.runtime\.paper-board\.io/tenant-id}'

To check the last time a tenant pod handled a request:

Terminal window
kubectl get deployment -n paper-board runtime-t-<short> \
-o jsonpath='{.metadata.annotations.runtime\.paper-board\.io/last-seen-at}'

ResourceRequestLimit
CPU100m500m
Memory128Mi512Mi

Tier-based resource limits are a Phase 5 item. Until then, all tenant pods use the same fixed profile.

Each tenant Deployment runs exactly 1 replica. HA (multiple replicas per tenant) is a Phase 6 item.

The router runs 3 replicas. The controller runs 1 replica.


Runtime is schema-less. There is no MIGRATION_DB_URL, no DATABASE_URL, and no runtime_app Postgres user. The cmd/migrator/main.go and migrations/schema/000001_init.up.sql exist only for service-template structural parity — the migration file contains a comment and no DDL.


Phase 7 introduces a gateway service that centralizes edge auth, rate limiting, and routing. When gateway ships, the runtime-router responsibilities (JWT verification, tenant dispatch) move to gateway. The per-tenant pod model and controller remain unchanged.