Skip to content

Commit 3f33e65

Browse files
k8s: fix BugBash infra-manifest findings (P0-W5-01, P1-W5-06/07, P2-W5)
P0-W5-01: deploy-rbac.yaml ClusterRole was missing batch/jobs — every Kaniko build Job the api creates (BatchV1().Jobs().Create/Get/Delete) is RBAC-Forbidden on a manifest rebuild. Added the batch/jobs rule. P1-W5-06: worker + provisioner livenessProbe pointed at /livez, which neither binary serves (only /healthz is registered) — 404 → SIGKILL crashloop every ~3 min. Repointed both liveness probes to /healthz and corrected the stale manifest comments that wrongly claimed "/livez 404s and liveness falls back to grace". P1-W5-07: worker manifest hardcoded ENVIRONMENT=development in prod — a 60x probe-amplification bug (resource_heartbeat at dev cadence). Changed to production. P2-W5: added resources: requests+limits to all three service Deployments (instant-api 250m/256Mi..1/1Gi, worker + provisioner 100m/128Mi..500m/512Mi). Without resource requests the HPA fires FailedGetResourceMetric and never scales (P1-W5-15). P2-W5 (T4): added DEPLOY_DOMAIN + CERT_ISSUER to the ConfigMap — code otherwise falls back to the hardcoded "instant.dev" default. P3-W5 (T4): removed dead dashboard-grpc :50052 residue (DASHBOARD_GRPC_PORT ConfigMap key, app.yaml containerPort + Service port) — dashboard-api was retired 2026-05-11. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent 4495c8b commit 3f33e65

5 files changed

Lines changed: 115 additions & 36 deletions

File tree

k8s/app.yaml

Lines changed: 40 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ metadata:
1919
# any delete-old-versions workflow that targets prod tags.
2020
instanode.dev/image-pin-retention-days: "730"
2121
spec:
22-
replicas: 1
22+
replicas: 2
2323
selector:
2424
matchLabels:
2525
app: instant-api
@@ -29,6 +29,17 @@ spec:
2929
app: instant-api
3030
spec:
3131
serviceAccountName: instant-api # grants deploy-manager ClusterRole (see deploy-rbac.yaml)
32+
# Spread replicas across nodes when possible — preferred (not required)
33+
# so a 1-node dev cluster still schedules both pods.
34+
affinity:
35+
podAntiAffinity:
36+
preferredDuringSchedulingIgnoredDuringExecution:
37+
- weight: 100
38+
podAffinityTerm:
39+
labelSelector:
40+
matchLabels:
41+
app: instant-api
42+
topologyKey: kubernetes.io/hostname
3243
initContainers:
3344
# Wait for platform DB to be ready before starting the API
3445
- name: wait-for-platform-db
@@ -50,7 +61,6 @@ spec:
5061
image: instant-api:local # built with: docker build -t instant-api:local .
5162
ports:
5263
- containerPort: 8080
53-
- containerPort: 50052
5464
volumeMounts:
5565
- name: docker-sock
5666
mountPath: /var/run/docker.sock
@@ -60,6 +70,16 @@ spec:
6070
env:
6171
- name: DOCKER_HOST
6272
value: unix:///var/run/docker.sock
73+
- name: DATABASE_URL
74+
valueFrom:
75+
secretKeyRef:
76+
name: instant-secrets
77+
key: DATABASE_URL
78+
- name: CUSTOMER_DATABASE_URL
79+
valueFrom:
80+
secretKeyRef:
81+
name: instant-secrets
82+
key: CUSTOMER_DATABASE_URL
6383
- name: JWT_SECRET
6484
valueFrom:
6585
secretKeyRef:
@@ -120,11 +140,6 @@ spec:
120140
secretKeyRef:
121141
name: instant-secrets
122142
key: PROVISIONER_SECRET
123-
- name: MIGRATOR_SECRET
124-
valueFrom:
125-
secretKeyRef:
126-
name: instant-secrets
127-
key: MIGRATOR_SECRET
128143
- name: R2_API_TOKEN
129144
valueFrom:
130145
secretKeyRef:
@@ -231,22 +246,34 @@ spec:
231246
initialDelaySeconds: 15
232247
periodSeconds: 30
233248
failureThreshold: 6 # 6 × 30s = 3 min before SIGKILL
249+
# Resource requests/limits — the api is build-heavy (it shells out
250+
# to Kaniko Job orchestration on /deploy/new), so it gets headroom.
251+
# The requests are also what the HPA needs to compute utilization;
252+
# without them the HPA fires FailedGetResourceMetric and never
253+
# scales (P1-W5-15).
254+
resources:
255+
requests:
256+
cpu: 250m
257+
memory: 256Mi
258+
limits:
259+
cpu: "1"
260+
memory: 1Gi
234261
---
235262
apiVersion: v1
236263
kind: Service
237264
metadata:
238265
name: instant-api
239266
namespace: instant
240267
spec:
268+
# Externally reached via the cluster's Ingress (ingress-nginx) — see
269+
# infra/k8s/api-ingress.yaml. The Service stays ClusterIP so the only
270+
# public entrypoint is the LB → Ingress → Service path.
271+
# For local dev / E2E: kubectl port-forward svc/instant-api 8080:8080
272+
# (NodePort retired 2026-05-11).
273+
type: ClusterIP
241274
selector:
242275
app: instant-api
243276
ports:
244277
- name: http
245278
port: 8080
246279
targetPort: 8080
247-
nodePort: 30080 # access locally at http://localhost:30080
248-
- name: dashboard-grpc
249-
port: 50052
250-
targetPort: 50052
251-
nodePort: 30052
252-
type: NodePort # Rancher Desktop exposes NodePort via localhost

k8s/configmap.yaml

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,25 +5,29 @@ metadata:
55
namespace: instant
66
data:
77
PORT: "8080"
8-
DASHBOARD_GRPC_PORT: "50052"
98
# OpenTelemetry OTLP gRPC (host:port, no scheme). Empty = tracing disabled (noop).
109
OTEL_EXPORTER_OTLP_ENDPOINT: ""
1110
OTEL_SERVICE_NAME: "instant-api"
1211
ENVIRONMENT: development
1312
INSTANT_ENABLED_SERVICES: redis,mongodb,postgres,queue,webhook,storage,deploy
1413
PLANS_PATH: /app/plans.yaml
1514
NATS_HOST: nats.instant-data.svc.cluster.local
16-
DATABASE_URL: postgres://instant:instant@postgres-platform.instant.svc.cluster.local:5432/instant_platform?sslmode=disable
1715
REDIS_URL: redis://redis.instant.svc.cluster.local:6379
1816
GEOLITE2_DB_PATH: /app/GeoLite2-City.mmdb
1917
GITHUB_CLIENT_ID: ""
2018
GOOGLE_CLIENT_ID: ""
2119
# Optional default for GET /auth/google/url (agent API) when redirect_uri query is omitted
2220
GOOGLE_REDIRECT_URI: ""
2321
PROVISIONER_ADDR: "instant-provisioner.instant-infra.svc.cluster.local:50051"
24-
MIGRATOR_ADDR: "http://instant-migrator.instant-infra.svc.cluster.local:8090"
2522
COMPUTE_PROVIDER: "k8s"
2623
STACK_EXPOSE_VIA: "nodeport"
2724
KUBE_NAMESPACE_APPS: "instant-apps"
2825
MINIO_ENDPOINT: "minio.instant-data.svc.cluster.local:9000"
2926
MINIO_BUCKET_NAME: "instant-shared"
27+
# Deploy / stack ingress wiring. DEPLOY_DOMAIN is the wildcard zone the
28+
# api builds per-deploy hostnames under (<id>.deployment.instanode.dev);
29+
# without it the code falls back to the hardcoded default "instant.dev".
30+
# CERT_ISSUER names the cert-manager ClusterIssuer used for TLS on those
31+
# ingresses.
32+
DEPLOY_DOMAIN: "deployment.instanode.dev"
33+
CERT_ISSUER: "letsencrypt-http01"

k8s/deploy-rbac.yaml

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,14 @@ rules:
3737
resources: ["deployments"]
3838
verbs: ["get", "list", "watch", "create", "update", "patch", "delete"]
3939

40+
# Batch Jobs — the api runs each Kaniko build as a Job inside the
41+
# per-deploy namespace (client.go BatchV1().Jobs(ns).Create/Get/Delete).
42+
# Without this rule every /deploy/new and /stacks/new build is
43+
# RBAC-Forbidden at Job creation.
44+
- apiGroups: ["batch"]
45+
resources: ["jobs"]
46+
verbs: ["get", "list", "watch", "create", "delete"]
47+
4048
# Ingresses (Traefik on k3s uses networking.k8s.io/v1).
4149
- apiGroups: ["networking.k8s.io"]
4250
resources: ["ingresses", "networkpolicies"]

k8s/provisioner/deployment.yaml

Lines changed: 24 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ metadata:
1414
annotations:
1515
instanode.dev/image-pin-retention-days: "730"
1616
spec:
17-
replicas: 1
17+
replicas: 2
1818
selector:
1919
matchLabels:
2020
app: instant-provisioner
@@ -26,6 +26,16 @@ spec:
2626
# ServiceAccount grants ClusterRole permissions for k8s dedicated backend.
2727
# When K8S_DEDICATED_BACKEND=false (default), the SA is still present but unused.
2828
serviceAccountName: instant-provisioner
29+
# Spread replicas across nodes so node loss doesn't take all gRPC handlers down.
30+
affinity:
31+
podAntiAffinity:
32+
preferredDuringSchedulingIgnoredDuringExecution:
33+
- weight: 100
34+
podAffinityTerm:
35+
labelSelector:
36+
matchLabels:
37+
app: instant-provisioner
38+
topologyKey: kubernetes.io/hostname
2939
initContainers:
3040
- name: wait-for-provisioner-db
3141
image: busybox:1.36
@@ -157,14 +167,11 @@ spec:
157167
secretKeyRef:
158168
name: minio-secrets
159169
key: MINIO_ROOT_PASSWORD
160-
# Probe split (2026-05-13): readiness hits the /healthz sidecar on
161-
# port 8092 (checks DB + pool init). Liveness hits /livez on the
162-
# same sidecar — process-only check that returns 200 unconditionally
163-
# so a slow pool warm-up can't kill a healthy pod. Startup probe
164-
# gives 10 minutes for pool pre-creation before liveness takes over.
165-
# See PR body for the api/provisioner follow-up adding GET /livez.
166-
# Until that handler ships, /livez 404s and liveness falls back to
167-
# failureThreshold=6 grace (≈3 min).
170+
# Probes (2026-05-18): the provisioner's /healthz sidecar on port
171+
# 8092 registers ONLY /healthz — it never serves /livez. All three
172+
# probes hit /healthz (checks DB + pool init). The generous
173+
# startupProbe budget (10 min) absorbs hot-pool pre-creation before
174+
# the liveness probe takes over.
168175
startupProbe:
169176
httpGet:
170177
path: /healthz
@@ -180,8 +187,15 @@ spec:
180187
failureThreshold: 3
181188
livenessProbe:
182189
httpGet:
183-
path: /livez # process-only; see startupProbe note
190+
path: /healthz # provisioner serves only /healthz (no /livez)
184191
port: 8092
185192
initialDelaySeconds: 15
186193
periodSeconds: 30
187194
failureThreshold: 6 # 6 × 30s = 3 min before SIGKILL
195+
resources:
196+
requests:
197+
cpu: 100m
198+
memory: 128Mi
199+
limits:
200+
cpu: 500m
201+
memory: 512Mi

k8s/worker/deployment.yaml

Lines changed: 36 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ metadata:
1414
annotations:
1515
instanode.dev/image-pin-retention-days: "730"
1616
spec:
17-
replicas: 1
17+
replicas: 2
1818
selector:
1919
matchLabels:
2020
app: instant-worker
@@ -24,20 +24,29 @@ spec:
2424
app: instant-worker
2525
spec:
2626
serviceAccountName: instant-worker
27+
# Multi-worker is safe — River queue uses Postgres SELECT FOR UPDATE
28+
# SKIP LOCKED so each job lands on exactly one worker.
29+
affinity:
30+
podAntiAffinity:
31+
preferredDuringSchedulingIgnoredDuringExecution:
32+
- weight: 100
33+
podAffinityTerm:
34+
labelSelector:
35+
matchLabels:
36+
app: instant-worker
37+
topologyKey: kubernetes.io/hostname
2738
containers:
2839
- name: worker
2940
image: instant-worker:local
3041
imagePullPolicy: Never
3142
ports:
3243
- containerPort: 8091
3344
name: liveness
34-
# Probe split (2026-05-13): /healthz checks River queue connectivity
35-
# + DB pool (slow on cold start); /livez is process-only. Worker
36-
# boots after migrations finish (it owns no migrations of its own),
37-
# but heavy job replay on restart can stall /healthz briefly.
38-
# See PR body for the worker follow-up adding GET /livez.
39-
# Until that handler ships, /livez 404s and liveness falls back to
40-
# failureThreshold=6 grace (≈3 min).
45+
# Probes (2026-05-18): the worker binary registers ONLY /healthz
46+
# (plus /metrics) — it never serves /livez. All three probes hit
47+
# /healthz. /healthz checks River queue connectivity + DB pool
48+
# (slow on cold start); heavy job replay on restart can stall it
49+
# briefly, which the generous startupProbe budget absorbs.
4150
startupProbe:
4251
httpGet:
4352
path: /healthz
@@ -53,11 +62,18 @@ spec:
5362
failureThreshold: 3
5463
livenessProbe:
5564
httpGet:
56-
path: /livez # process-only; see startupProbe note
65+
path: /healthz # worker serves only /healthz (no /livez)
5766
port: 8091
5867
initialDelaySeconds: 15
5968
periodSeconds: 30
6069
failureThreshold: 6 # 6 × 30s = 3 min before SIGKILL
70+
resources:
71+
requests:
72+
cpu: 100m
73+
memory: 128Mi
74+
limits:
75+
cpu: 500m
76+
memory: 512Mi
6177
env:
6278
# ── Observability (track 6 of 8) ──────────────────────────────────
6379
# NEW_RELIC_LICENSE_KEY pulled from instant-infra-secrets. When
@@ -97,7 +113,7 @@ spec:
97113
name: instant-infra-secrets
98114
key: PROVISIONER_SECRET
99115
- name: ENVIRONMENT
100-
value: development
116+
value: production
101117
- name: KUBE_NAMESPACE_APPS
102118
value: "instant-apps"
103119
# MinIO storage_bytes scanner — added 2026-05-11 in worker
@@ -123,3 +139,13 @@ spec:
123139
name: instant-infra-secrets
124140
key: MINIO_ROOT_PASSWORD
125141
optional: true
142+
# Brevo sender identity for the raw-HTML send path (added
143+
# 2026-05-15). The dashboard-template path was inheriting a
144+
# personal email from the Brevo sender field — sending these
145+
# via code-controlled env vars closes that bug. Defaults in
146+
# brevo_provider.go (noreply@instanode.dev / "instanode")
147+
# take over if the env var is absent.
148+
- name: BREVO_SENDER_EMAIL
149+
value: "noreply@instanode.dev"
150+
- name: BREVO_SENDER_NAME
151+
value: "instanode"

0 commit comments

Comments
 (0)