- Kotlin 86%
- TypeScript 10%
- CSS 2.1%
- Shell 0.8%
- Dockerfile 0.8%
- Other 0.3%
| Filename | Latest commit message | Latest commit date |
|---|---|---|
| .forgejo/workflows | ||
| frontend | ||
| gradle/wrapper | ||
| jenkins | ||
| k8s | ||
| monitoring | ||
| nginx | ||
| scripts | ||
| src | ||
| .dockerignore | ||
| .gitignore | ||
| build.gradle.kts | ||
| docker-compose.yml | ||
| Dockerfile | ||
| Dockerfile.ci | ||
| gradlew | ||
| gradlew.bat | ||
| lab8-answersheet.md | ||
| linklab.postman_collection.json | ||
| README.md | ||
| settings.gradle.kts | ||
LinkLab — Week 8 Blueprint
CS4297 Applied System Design · Week 8 — Saying What You Want, Not How To Get It
Complete Week 7 solution included. Clone this and start here.
What you are building this week
| Part | The problem it poses | Time |
|---|---|---|
| A | Nobody is going to restart it for you | 0:00–0:30 |
| B | A process can be running and useless. Who decides it is healthy? | 0:30–0:55 |
| C | You just ran four copies. What actually got four times better? | 0:55–1:30 |
| D | So what would it take to replicate the part that matters? | 1:30–2:00 |
Part D is written and discussed — no commands. The lab sheet is the authority on what happens when; this table is orientation only.
Prerequisites
0. Push this repo and let CI build your image — DO THIS BEFORE THE LAB
Part C verifies that JWT works across pods, so the image you deploy must contain the Week 7 auth code. The image your Week 5 pipeline pushed does not — JWT did not exist until Week 7. Deploy an image built from this repo.
A fresh clone has nothing new to push, so a plain git push does nothing and
CI never runs. Give it a commit:
git commit --allow-empty -m "Week 8: trigger build"
git push origin main # triggers .forgejo/workflows/ci.yml
Then Forgejo → your repo → Packages and confirm a package appears. The CI
pushes cs4297.skynet.ie/<your-username>/<your-repo-name>:latest.
This takes a few minutes. Do it at home, not at 09:05 on Tuesday.
1. Enable Kubernetes in Docker Desktop
Settings → Kubernetes → Enable Kubernetes → cluster type kubeadm (not kind) → Apply & Restart
On the kind cluster type, images are pulled through a local mirror that
ignores your forgejo-registry secret entirely, and every pod sits in
ImagePullBackOff.
Verify: kubectl get nodes — it should show exactly one node,
docker-desktop, Ready. Three nodes means you are on kind.
Give Docker at least 8 GB. Settings → Resources → Memory. You are about to run MySQL, Redis, DynamoDB, ElasticSearch and Kafka and a Kubernetes control plane and two application pods at 768 MiB each. At the 2–4 GB default, pods are evicted and you will debug the wrong problem all afternoon.
2. Start Docker Compose databases
docker compose up -d mysql redis dynamodb-local elasticsearch kafka
3. Verify host.docker.internal works
kubectl run test --image=curlimages/curl --restart=Never -it --rm \
-- curl -sf http://host.docker.internal:9200 2>/dev/null \
&& echo "ElasticSearch reachable" || echo "PROBLEM"
(Uses ElasticSearch, not MySQL — MySQL speaks a binary protocol on 3306, not
HTTP, so curl against it always fails even when the connection is fine.
ElasticSearch answers plain HTTP on 9200, so this actually tests what it
claims to.)
If not reachable: Docker Desktop → Settings → Resources → Network → enable "host.docker.internal should resolve to the host" → restart.
Kubernetes manifests
k8s/
├── 00-namespace.yml # linklab namespace
├── 01-secret.yml # JWT secret, DB passwords
├── 02-configmap.yml # non-sensitive config, host.docker.internal URLs
├── 03-deployment.yml # 2 replicas, readiness + liveness probes
├── 04-service.yml # ClusterIP service (use port-forward to access locally)
├── 05-hpa.yml # not used in this year's lab (autoscaling is CS4135)
└── broken/
└── deployment-broken.yml # not used this year — Part B now has you break
# the readiness probe in 03-deployment.yml yourself
Quick kubectl reference
# Apply a manifest
kubectl apply -f k8s/03-deployment.yml
# Watch pods
kubectl get pods -n linklab -w
# Describe a pod (events, env vars, probes)
kubectl describe pod <pod-name> -n linklab
# View logs
kubectl logs <pod-name> -n linklab
kubectl logs <pod-name> -n linklab --previous # crashed container
# Execute a command inside a pod
kubectl exec -n linklab <pod-name> -- curl localhost:8080/actuator/health
# Port-forward to reach the service locally
kubectl port-forward svc/linklab -n linklab 8090:80
# Undo the last change to a deployment (Part B uses this)
kubectl rollout undo deployment/linklab -n linklab
# Scale manually
kubectl scale deployment linklab --replicas=3 -n linklab
# Clean up everything
kubectl delete namespace linklab
Finding your Forgejo image tag
The registry host is cs4297.skynet.ie — the same REGISTRY value in
.forgejo/workflows/ci.yml. CI pushes ${REGISTRY}/${{ forgejo.repository }},
so the image name is your repository name, not a fixed product name:
cs4297.skynet.ie/<your-username>/<your-repo-name>:latest
To confirm yours: Forgejo → your repository → Packages tab. Copy the path shown there verbatim — do not type it from memory.
Put that exact path into the image: value in 03-deployment.yml. That is
the only file that needs it.
Finding your JWT in the browser (alternative to C.1)
Only if you are also running the frontend (docker compose up -d frontend,
then log in at http://localhost:3001) -- the lab itself does not start it.
After logging in:
- Open DevTools (F12)
- Application tab → Local Storage → http://localhost:3001
- Click
token→ copy the value
Use it as: Authorization: Bearer <token>
Common issues
Pods stuck in Pending / ImagePullBackOff
Check kubectl describe pod -n linklab and read the Events at the bottom.
In order of likelihood:
- The image path is wrong. It must be
cs4297.skynet.ie/<username>/<repo-name>:latest, copied from your Forgejo Packages tab. A wrong host or a wrong repo name both look like this. - CI never ran, so nothing was pushed. Check your repo's Actions tab.
- The pull secret is wrong. Most often the token has no package
permission: it still logs in fine, but every pull gets
401. Use the Week 5 token withwrite:packages, then delete and recreateforgejo-registrywith--namespace=linklab. - Insufficient memory: Docker Desktop → Settings → Resources (see above).
host.docker.internal not resolving from pods
Enable in Docker Desktop → Settings → Resources → Network.
Restart Docker Desktop after changing this setting.
CrashLoopBackOff on correct deployment (not Part B)
Two causes, and they look identical:
- A database is unreachable.
kubectl logs <pod> -n linklabnames it. Verify withdocker compose ps. - The app was still starting when the liveness probe killed it. Spring Boot
booting against five datastores on a busy laptop can take over two minutes.
kubectl logs <pod> --previousshowing a normal, incomplete startup with no error is the signature. RaiselivenessProbe.initialDelaySeconds.
A pod shows Running but never 1/1 READY
The container is up but the readiness probe keeps failing, so the Service
sends it nothing. kubectl describe pod -n linklab and read the Events —
kubectl logs will look perfectly normal, which is Part B's whole point.
Week-by-week progression
| Week | Content | Blueprint |
|---|---|---|
| 2 | REST API + MySQL | Stubs for LINK-001 |
| 3 | Docker · Redis · NGINX · Grafana | Complete Week 2 |
| 4 | SQL vs NoSQL · DynamoDB | Complete Week 3 |
| 5 | CI/CD · ElasticSearch | Complete Week 4 |
| 6 | Kafka async messaging | Complete Week 5 |
| 7 | JWT + OAuth2 + RBAC | Complete Week 6 |
| 8 | Kubernetes | ← you are here |
| 9 | — | Complete Week 8 |
| 10 | Capstone | — |