Operations
Operations
Section titled “Operations”Environment variables
Section titled “Environment variables”All runtime binaries read configuration from environment variables via sdk/config.MustBind. Variables are injected by Helm ConfigMap → Deployment env block.
Shared (all binaries)
Section titled “Shared (all binaries)”| Variable | Default | Description |
|---|---|---|
GRPC_PORT | 50051 | gRPC listen port (server + tenant pods). |
LOG_LEVEL | info | Log verbosity. One of: debug, info, warn, error. |
OTEL_EXPORTER_OTLP_ENDPOINT | (empty) | OTLP endpoint for traces/metrics. Empty disables export. |
ENV | dev | Deployment environment. One of: dev, staging, prod. |
Controller
Section titled “Controller”| Variable | Default | Description |
|---|---|---|
IDENTITY_GRPC_ADDR | identity.paper-board.svc.cluster.local:50051 | gRPC target for identity.AuthService (used to verify tenant existence). |
TENANT_POD_IMAGE | ghcr.io/paper-board/runtime-server:v0.1.0 | Container image for per-tenant runtime Deployments. |
TENANT_POD_CPU_REQUEST | 100m | CPU request for tenant pods. |
TENANT_POD_CPU_LIMIT | 500m | CPU limit for tenant pods. |
TENANT_POD_MEMORY_REQUEST | 128Mi | Memory request for tenant pods. |
TENANT_POD_MEMORY_LIMIT | 512Mi | Memory limit for tenant pods. |
K8S_NAMESPACE | paper-board | Kubernetes namespace where tenant Deployments live. |
K8S_DNS_SUFFIX | paper-board.svc.cluster.local | Cluster DNS suffix appended to tenant Service names. |
IDLE_TIMEOUT_MINUTES | (empty) | Override idle timeout in minutes. Empty = 60 minutes. Intended for tests. |
Router
Section titled “Router”| Variable | Default | Description |
|---|---|---|
ROUTER_HTTP_PORT | 8080 | HTTP listen port for the router. |
IDENTITY_GRPC_ADDR | identity.paper-board.svc.cluster.local:50051 | gRPC target for JWT verification. |
TENANT_GRPC_PORT | 50051 | gRPC port on tenant pods that the router dials. |
K8S_NAMESPACE | paper-board | Namespace to look up tenant Deployments in. |
K8S_DNS_SUFFIX | paper-board.svc.cluster.local | DNS suffix used when building per-tenant gRPC targets. |
Helm values
Section titled “Helm values”Chart: helm/runtime/ — published as oci://ghcr.io/paper-board/charts/runtime.
Server (tenant pod template)
Section titled “Server (tenant pod template)”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
Section titled “Controller”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
Section titled “Router”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.
Tenant pod defaults
Section titled “Tenant pod defaults”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: 3600tenantPod 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
Section titled “Migrator”migrator: enabled: falseRuntime has no Postgres schema (ADR-0002). The migrator Job is disabled. The enabled: false block is retained for service-template structural parity only.
Tenant-lifecycle-controller deployment
Section titled “Tenant-lifecycle-controller deployment”The controller runs as a single-replica Deployment in the paper-board namespace. It requires a ClusterRole with the following permissions:
| Resource | Verbs |
|---|---|
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
| Sweep | Frequency | Trigger |
|---|---|---|
| Idle sweep | Every 5 minutes | Internal ticker in RunSweepLoop |
| Weekly sweep | Every 24 hours | Second ticker in RunSweepLoop |
The controller blocks on RunSweepLoop until context cancellation. Graceful shutdown waits for the current sweep iteration to finish.
Pod naming and discovery
Section titled “Pod naming and discovery”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:50051tenant_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:
kubectl get deployments -n paper-board -l runtime.paper-board.io/tenant=trueTo find which tenant a Deployment belongs to:
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:
kubectl get deployment -n paper-board runtime-t-<short> \ -o jsonpath='{.metadata.annotations.runtime\.paper-board\.io/last-seen-at}'Scaling and resource limits
Section titled “Scaling and resource limits”Tenant pod resources (Phase 3 fixed)
Section titled “Tenant pod resources (Phase 3 fixed)”| Resource | Request | Limit |
|---|---|---|
| CPU | 100m | 500m |
| Memory | 128Mi | 512Mi |
Tier-based resource limits are a Phase 5 item. Until then, all tenant pods use the same fixed profile.
Replicas
Section titled “Replicas”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.
No-schema note
Section titled “No-schema note”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 transition
Section titled “Phase 7 transition”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.