- Python 100%
| .forgejo | ||
| apps | ||
| argo-cd | ||
| docs | ||
| talos | ||
| .gitignore | ||
| README.md | ||
talos-infra
GitOps-driven infrastructure for a 3-node Kubernetes cluster on Talos Linux, virtualized on a Proxmox VE host.
Architecture
- 3-node Talos Linux cluster (1 control plane + 2 workers), KVM VMs on Proxmox
- Static IPs: control plane
10.120.1.150, workers10.120.1.151/10.120.1.152 - MetalLB (layer 2) hands out
10.120.1.153-253to LoadBalancer services - No ingress controller: every exposed app gets its own MetalLB IP
- External access via NetBird reverse proxy on a VPS, TLS terminated there
- ArgoCD (app-of-apps) watches this repository and syncs the cluster
- CI/CD: Forgejo Actions (
act_runnerinside the cluster) builds images and pushes to the in-cluster registry
push -> Forgejo (10.120.1.109) -> act_runner builds image
-> in-cluster registry -> bump image tag in this repo
-> ArgoCD syncs -> rollout
Repo layout
apps/
root.yaml # app-of-apps entry point (applies everything here)
metal-lb.yaml # MetalLB controller (helm chart)
metal-lb-config.yaml # IP pool + L2 advertisement
local-path-provisioner.yaml
registry.yaml # private container registry
metal-lb-config/ # IPAddressPool + L2Advertisement
registry/ # registry:2 deployment, ClusterIP service, PVC
argo-cd/ # bootstrap values for the ArgoCD helm install
Bootstrap (one-time, documented in docs/argo-cd-bootstrap.md)
- Install ArgoCD via helm with
argo-cd/values.yaml kubectl apply -f apps/root.yaml- ArgoCD syncs every Application in this repo from then on
Adding a new app
- Add
apps/<app>.yaml(an ArgoCD Application) + anapps/<app>/manifest dir - Push — ArgoCD creates it; give the app a LoadBalancer Service for a MetalLB IP and point a NetBird reverse proxy entry at it
Rollbacks
Every CI run produces one image-tag commit in this repo (chained on the previous one), so rolling back to the previous release is a single revert:
git revert <image-bump-commit>
git push
ArgoCD picks the change up (webhook or the 60-second reconciliation window) and rolls the deployment back to the previously pinned image, which still exists in the registry. Reverting only the latest bump commit is safe; older reverts may restore an image tag that no longer exists in the registry.
Escape hatch: ArgoCD UI -> application -> Sync to a previous revision.
Update bot
A nightly workflow (.forgejo/workflows/update-bot.yaml) checks the image
tags in apps/, reads release notes, and opens a pull request for each
update. It merges automatically only when the update is a patch or minor
bump on an already pinned tag and the risk check approves; first pins from
floating tags (e.g. latest, 2) and major bumps always wait for a human.
The bot's config lives in .forgejo/update-bot/apps.json — adding an app
to the bot is one entry there:
{
"app": "name",
"image": "owner/image",
"manifest": "apps/name/deployment.yaml",
"notes": "https://api.github.com/repos/owner/repo/releases",
"floating": "latest"
}
Secrets used (Forgejo repo secrets): LLM_API_KEY, LLM_BASE_URL,
LLM_MODEL, INFRA_TOKEN. The risk check fails safe: no verdict means
the pull request waits for a human.
Observability
- kube-prometheus-stack (Prometheus + Grafana), trimmed for 2-core nodes
- Grafana:
http://10.120.1.156, admin password in thegrafana-admin-credentialssecret (created via kubectl, not in git) - Prometheus: 7d retention, 10Gi local-path volume
- Quirk: the operator mounts the DB volume with
subPath: prometheus-db, and the kubelet recreates that dir as root:0755 at pod start. A root init container (fix-db-perms) chmods it to 0777 on every start.
Secrets policy
Nothing secret is committed. Registry htpasswd, webhook tokens, and runner
registration tokens are created directly as Kubernetes Secrets via kubectl.
Services
- linkding — bookmark manager (
10.120.1.157) - freshrss — RSS reader (
10.120.1.158) - registry — container registry (
10.120.1.154) - site — melonsoda.org (
10.120.1.155) - lexsteen-site — Alfredo Sasso portfolio (
10.120.1.159)
The update bot opens pull requests for each update; safe patch/minor bumps on pinned tags are merged automatically after the LLM risk check approves.