K8s manifests and ArgoCD applications for student projects on the UNICAMP IC cluster (ak8s.ic.unicamp.br).
.
├── apps/ # Deployed applications
│ └── mih-server/
│ ├── base/ # Kustomize base — shared manifests
│ └── overlays/
│ └── prod/ # Production overlay (namespace, secrets, image tag)
├── argocd/ # ArgoCD Application definitions (one per app)
└── templates/
├── app/ # Template for new apps (copy this)
│ ├── base/
│ └── overlays/prod/
├── argocd/ # ArgoCD Application template
│ └── argocd-app.yaml
└── workflows/ # GitHub Actions templates for app repos
apps/seaweedfs/ runs a single-pod SeaweedFS (weed server -filer -s3) that backs
image uploads for wounds-server. It is a third-party image pinned by hand in
base/statefulset.yaml — no CI tag bump.
Design notes worth knowing before changing it:
- One StatefulSet, not four Deployments. The compose file in
server-woundssplits master/volume/filer/s3 into separate containers sharing one volume; that cannot be translated to k8s, since a RWO PVC is not shareable across pods. weed s3started without-s3.configdefaults to Open Access — every access key is accepted and the gateway is world-readable and world-writable. Theseaweedfs-secretholdings3config.jsonis what prevents this. Never expose the Ingress without it.- No
anonymousidentity is defined, so unauthenticated reads are denied. Clients read exclusively through presigned URLs generated by Django. - Never add
nginx.ingress.kubernetes.io/rewrite-targetto the seaweed Ingress. Path-style addressing signs the full request path into the SigV4 signature; a rewrite silently invalidates every presigned URL. wounds-configpointsAWS_S3_ENDPOINT_URLat the public Ingress host, not the internal Service. django-storages signs the Host header into the URL, so Django must use the same host the browser will fetch from.- The default
microk8s-hostpathStorageClass hasALLOWVOLUMEEXPANSION=false— the PVC cannot be grown later.
Apply order matters: create the namespace and apply secret.yaml before
pushing argocd/seaweedfs-app.yaml, or the pod hangs in ContainerCreating.
-
Copy template:
cp -r templates/app apps/<your-app>
-
Replace placeholders in all files under
apps/<your-app>/:<APP_NAME>— your app name (e.g.my-api)<NAMESPACE>— K8s namespace (e.g.my-api)<GH_USER_OR_ORG>— GitHub user/org owning the image<APP_PORT>— container port (e.g.8000)
-
Configure your app:
- Add env vars to
base/configmap.yaml - If app needs DB, copy
apps/mih-server/base/postgres.yamlas reference and uncomment inbase/kustomization.yaml
- Add env vars to
-
Create ArgoCD Application:
cp templates/argocd/argocd-app.yaml argocd/<your-app>-app.yaml
Replace placeholders in the new file.
-
Commit + push infra repo.
-
Apply secret manually on cluster (one-time, secret is NOT in git):
cp templates/app/overlays/prod/secret-example.yaml secret.yaml # local only # fill secret.yaml with real values kubectl create namespace <NAMESPACE> kubectl apply -f secret.yaml -n <NAMESPACE>
-
Done — no per-app
kubectl apply. The app-of-appsrootApplication (argocd/root-app.yaml) auto-discovers anyargocd/*-app.yamlonmainand syncs it. Just commit + push (step 5).First-time cluster setup only — apply the root once:
kubectl apply -f argocd/root-app.yaml
app repo infra repo (this) cluster
───────── ────────────────── ────────
push to main
│
▼
build image (CI)
│
▼
push to ghcr.io
│
▼
trigger workflow ───────► bump image tag in
overlays/prod/
kustomization.yaml
│
▼
commit + push
│
▼
ArgoCD detects ───────────► sync to cluster
In each app repo, copy templates/workflows/deploy-k8s.yml to .github/workflows/.
One job: builds the Docker image, pushes to GHCR, then bumps the tag in this repo
(ArgoCD syncs from there). Frontends uncomment the build-args block for their
build-time VITE_* vars.
Required in app repo settings (Settings → Secrets and variables → Actions):
- Secret
INFRA_APP_ID— GitHub App ID (see "GitHub App setup" below) - Secret
INFRA_APP_PRIVATE_KEY— full.pemfile contents - Variable
APP_NAME— folder name underapps/(e.g.mih-server)
The infra repo is updated by a GitHub App so commits don't rely on any individual user's PAT.
github.com/organizations/datasci4citizens/settings/apps → New GitHub App- Name:
infra-deployer - Webhook: disable
- Repository permissions → Contents: Read and write
- Where installable: "Only on this account"
- Create → note App ID + generate Private Key (downloads
.pem) - Install App on
server-infrastructurerepo - Share App ID and
.pemcontents with team members; each app repo stores them as secretsINFRA_APP_IDandINFRA_APP_PRIVATE_KEY
Secrets are NOT stored in git. Workflow:
secret-example.yaml(committed) — template showing required keyssecret.yaml(gitignored) — real values, applied manually to cluster once- ArgoCD manages everything else; references the existing
<APP_NAME>-secretin namespace
For production-grade secret management in git, look into Sealed Secrets or SOPS.
kubectl apply -k apps/<your-app>/overlays/prod