e2bcfff5ab
The skills-review desk is static, so "which draft would you ship?" needs a stateful counterpart. vote-service is a small Go API on its own pod backed by a JSON file on a ReadWriteOnce PVC, with one active vote per skill per source IP as the anti-abuse rule and CORS (ALLOWED_ORIGIN) as the caller boundary. Deployment notes that differ from the obvious path, all confirmed against the live cluster: the image is side-loaded with `ctr image import` plus `imagePullPolicy: Never` because kubelet has no credentials for the Nexus ref; the pod is pinned to `kubernets` because the hostpath PV takes a nodeAffinity for whichever node first binds it; and public exposure is Caddy on the VPS, not the cloudflared tunnel. The ingress controller runs with `use-forwarded-headers` off, so nginx overwrites X-Forwarded-For with its own peer — every visitor would collapse into one voter and each skill would cap at one vote overall. Caddy stamps the true remote address into X-Client-IP, which nginx forwards untouched, and clientIP() reads that first. Scoped to this app rather than flipping the global flag, which would change client-IP handling for every other ingress. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
107 lines
3.9 KiB
YAML
107 lines
3.9 KiB
YAML
# Namespace, image ref, and storage class confirmed against this cluster
|
|
# (microk8s, 2026-09-04). Image is pushed to Nexus for a durable, off-node
|
|
# copy (docker push localhost:30892/... — see README), but the Deployment
|
|
# below pulls it from the *node's local containerd image store* instead of
|
|
# over the network: kubelet's image pulls run in the host network namespace,
|
|
# which uses this node's public DNS resolver, not cluster CoreDNS, so
|
|
# `nexus-service.nexus.svc.cluster.local` is NOT resolvable for a plain pull
|
|
# (only for in-cluster builders like Kaniko, whose *build* pod runs in pod
|
|
# netns). The `microk8s-hostpath` PVC below also pins every pod to whichever
|
|
# node created it (`ai-workstation`, confirmed via the PV's nodeAffinity), so
|
|
# a single local `ctr image import` of the pushed tar is enough — see
|
|
# vote-service/README.md for the import command. `imagePullPolicy: Never`
|
|
# enforces that: no accidental network pull attempt, no ImagePullBackOff.
|
|
# `ai-for-dummies` did not exist yet as a namespace, so it is created below,
|
|
# matching the one-namespace-per-app pattern every other small app in this
|
|
# cluster uses (judge0, minio, pragent, …). No storageClassName set:
|
|
# microk8s's `hostpath-storage` addon is the default.
|
|
apiVersion: v1
|
|
kind: Namespace
|
|
metadata:
|
|
name: ai-for-dummies
|
|
---
|
|
apiVersion: apps/v1
|
|
kind: Deployment
|
|
metadata:
|
|
name: ai-for-dummies-vote
|
|
namespace: ai-for-dummies
|
|
labels:
|
|
app: ai-for-dummies-vote
|
|
spec:
|
|
replicas: 1 # single replica: the store is one JSON file on one PVC, not a shared DB
|
|
selector:
|
|
matchLabels:
|
|
app: ai-for-dummies-vote
|
|
strategy:
|
|
type: Recreate # avoid two pods writing the same PVC-backed file at once
|
|
template:
|
|
metadata:
|
|
labels:
|
|
app: ai-for-dummies-vote
|
|
spec:
|
|
# Pinned to `kubernets`: the image is imported straight into that node's
|
|
# containerd store (see README) and `microk8s-hostpath` PVs carry a
|
|
# nodeAffinity for whichever node first binds them, so scheduling and
|
|
# storage must agree on one node. `kubernets` is the control-plane node
|
|
# that hosts the rest of this cluster's workloads.
|
|
nodeSelector:
|
|
kubernetes.io/hostname: kubernets
|
|
securityContext:
|
|
fsGroup: 65532 # matches distroless "nonroot" uid/gid; without it the PVC mounts root-owned and the container can't write votes.json
|
|
containers:
|
|
- name: vote-service
|
|
image: localhost:30892/ai-for-dummies-vote-service:latest
|
|
imagePullPolicy: Never # image is side-loaded via `ctr image import`; never fetch over the network
|
|
ports:
|
|
- containerPort: 8080
|
|
env:
|
|
- name: PORT
|
|
value: "8080"
|
|
- name: VOTE_DB_PATH
|
|
value: /data/votes.json
|
|
- name: ALLOWED_ORIGIN
|
|
value: https://netcracker.pages.marcospaulo.dev.br
|
|
resources:
|
|
requests: { cpu: 10m, memory: 16Mi }
|
|
limits: { cpu: 100m, memory: 64Mi }
|
|
readinessProbe:
|
|
httpGet: { path: /healthz, port: 8080 }
|
|
initialDelaySeconds: 2
|
|
livenessProbe:
|
|
httpGet: { path: /healthz, port: 8080 }
|
|
initialDelaySeconds: 5
|
|
volumeMounts:
|
|
- name: data
|
|
mountPath: /data
|
|
securityContext:
|
|
runAsNonRoot: true
|
|
readOnlyRootFilesystem: true
|
|
allowPrivilegeEscalation: false
|
|
volumes:
|
|
- name: data
|
|
persistentVolumeClaim:
|
|
claimName: ai-for-dummies-vote-data
|
|
---
|
|
apiVersion: v1
|
|
kind: PersistentVolumeClaim
|
|
metadata:
|
|
name: ai-for-dummies-vote-data
|
|
namespace: ai-for-dummies
|
|
spec:
|
|
accessModes: ["ReadWriteOnce"]
|
|
resources:
|
|
requests:
|
|
storage: 100Mi
|
|
---
|
|
apiVersion: v1
|
|
kind: Service
|
|
metadata:
|
|
name: ai-for-dummies-vote
|
|
namespace: ai-for-dummies
|
|
spec:
|
|
selector:
|
|
app: ai-for-dummies-vote
|
|
ports:
|
|
- port: 80
|
|
targetPort: 8080
|