Files
ai-for-dummies/vote-service/deploy/deployment.yaml
Marcos Paulo e2bcfff5ab feat: add reader vote widget and vote-service
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>
2026-09-04 23:49:55 +00:00

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