A small URL shortener — Node.js + Redis — deployed to Kubernetes the way a real service would be. The app is intentionally tiny; the point is the operational shape around it: correct health probes, graceful shutdown, autoscaling, config and secrets, a disruption budget, and Kustomize overlays — the things that separate "it runs in a container" from "it runs in production."
Ingress ──► Service ──► shortlink (Node, 3 replicas) ──► Redis
shortlink.local /healthz /readyz /metrics
| Concern | Where | Why it matters |
|---|---|---|
| Liveness vs readiness done right | src/server.js, k8s/base/deployment.yaml |
/healthz (liveness) is dependency-free so a Redis blip never triggers a restart; /readyz (readiness) checks Redis and flips to 503 while draining, so traffic is steered away without a restart |
| Graceful shutdown | src/server.js |
On SIGTERM: stop reporting ready → drain in-flight requests → close Redis → exit, with a force-exit timer. Paired with terminationGracePeriodSeconds + a preStop hook for zero-dropped-request rollouts |
| Config & secrets | configmap.yaml, secret.yaml, src/config.js |
12-factor: all config from env, non-secret values in a ConfigMap, the Redis password from a Secret |
| Autoscaling | hpa.yaml |
CPU-based HorizontalPodAutoscaler, 3→10 replicas |
| Safe rollouts | deployment.yaml |
maxUnavailable: 0 rolling updates + readiness gating = no dropped requests during a deploy |
| Disruption budget | pdb.yaml |
A node drain or upgrade can't take the whole service down |
| Container hardening | deployment.yaml, Dockerfile |
Non-root, read-only root filesystem, all capabilities dropped, multi-stage minimal image, tini as PID 1 for clean signals |
| Observability | src/server.js |
Prometheus /metrics (request histogram, labelled by route pattern to bound cardinality) + structured JSON logs |
| Environment overlays | k8s/overlays/local |
Kustomize base + a local overlay that patches replicas/PDB down for a laptop cluster |
Needs Docker, kind, and kubectl.
make demo # build image, create cluster, install ingress + metrics-server, deploy
echo "127.0.0.1 shortlink.local" | sudo tee -a /etc/hosts
make smoke # create a short link and follow the redirect through the ingressThen try the things the manifests are built for:
kubectl get pods -l app=shortlink # 3 replicas (1 on the local overlay)
kubectl rollout restart deployment/shortlink # watch a zero-downtime rolling update
curl http://shortlink.local/ # the "instance" field shows which pod answered| Method | Path | Does |
|---|---|---|
POST |
/api/links |
{ "url": "https://..." } → { code, shortUrl } |
GET |
/:code |
302 redirect to the original URL (increments a hit counter) |
GET |
/api/links/:code |
{ code, url, hits } |
GET |
/healthz /readyz /metrics |
liveness / readiness / Prometheus |
npm install
npm test # 9 tests against an in-memory store — no Redis needed
npm run dev # local server on :8080 (point REDIS_HOST at a local Redis)This is a showcase, so a few things are simplified on purpose. For a real deployment I'd change:
- Redis: the demo runs one Redis pod with ephemeral storage. Real: a managed Redis (ElastiCache / MemoryStore) or a StatefulSet with a PersistentVolumeClaim and replication.
- Secrets: the password is in
secret.yamlfor a one-command quickstart. Real: Sealed Secrets, External Secrets Operator, or a cloud secret manager — never plaintext in git. - Ingress/TLS: add cert-manager for real certificates; the demo is plain HTTP.
- Network policy: lock down so only the API can reach Redis.
- Image: pin a digest and scan it (Trivy) in CI; here it's a floating
:devtag loaded straight into kind.
These are listed because knowing the boundary of a demo is part of the point.
MIT — see LICENSE.