6.3 KiB
ArgoCD — GitOps for K3s
Argo CD is a declarative continuous delivery tool for Kubernetes. In this homelab it's the bridge between this repo and the K3s cluster: git is the source of truth, and Argo CD continuously watches the git repository, reconciling the cluster to match whatever manifests are tracked on main.
Add a Deployment, change a resource limit, delete an Ingress — commit it, and Argo CD pulls the change into the cluster automatically, no manual kubectl apply required. It's Kubernetes-as-GitOps rather than Kubernetes-as-state-you-manually-maintain.
Repo layout
Argo CD apps in this repo follow a split structure:
kubernetes/
├── apps/
│ └── homepage/
│ ├── deployment.yaml
│ ├── service.yaml
│ ├── configmap.yaml
│ ├── ingress.yaml
│ └── kustomization.yaml
└── argocd-apps/
└── homepage/
└── homepage-app.yaml
kubernetes/apps/<name>/— the actual Kubernetes workload manifests for each app. Bundled withkustomization.yaml(Kustomize), so common operations like renaming a namespace or swapping an image tag can be done via patches rather than editing every YAML. Seekubernetes/apps/README.mdfor details.kubernetes/argocd-apps/<name>/<name>-app.yaml— the Argo CDApplicationobject that tells Argo CD "watch this git path, deploy it into this namespace." These live one level up, deliberately separate fromkubernetes/apps/, so Argo CD never watches its own Application definitions and can't recurse.
The first app managed this way is the Homepage dashboard — see kubernetes/apps/homepage/ for the manifests and kubernetes/argocd-apps/homepage/homepage-app.yaml for the Application.
Application manifest
Each app gets a thin Application manifest. The Homepage one looks like:
apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
name: homepage
namespace: argocd
spec:
project: default
source:
repoURL: https://github.com/rezayaghobi-dev/homelab
targetRevision: main
path: kubernetes/apps/homepage
destination:
server: https://kubernetes.default.svc
namespace: homepage
syncPolicy:
automated:
prune: true
selfHeal: true
syncOptions:
- CreateNamespace=true
pathpoints at thekubernetes/apps/<name>/directory — Argo CD treats thekustomization.yamlthere as the entry point.automated.prune = true— anything deleted from git gets deleted from the cluster.automated.selfHeal = true— if someone edits the live cluster out-of-band, Argo CD notices the drift and corrects it.CreateNamespace=true— the destination namespace is created automatically if it doesn't exist.
This is the current state of the cluster's GitOps control loop: Argo CD is the reconciler, this repo is the spec, and the cluster state is the realized output.
How traffic reaches an app
K3s itself doesn't own ports 80/443 — the Docker-side Traefik reverse proxy already binds them (see Traefik). So the exposure pattern for an Argo CD-managed app is:
Client → Traefik (:443) → ingress-nginx NodePort → Ingress → Service → Pod
- Traefik (Docker container,
network_mode-adjacent to the host) terminates TLS (*.home.servervia mkcert) and routes based onHostheader. - ingress-nginx runs inside the K3s cluster. Its
Serviceis exposed as a NodePort on the host, giving Traefik a stablehomeserver:<nodeport>target to forward to — the same pattern used for pre-ArgoCD K3s workloads (see K3s — Exposure pattern). - ingress-nginx (the in-cluster Ingress controller) receives the request, matches it against
Ingressresources in the cluster, and forwards to the rightService. - Service → Pod as normal Kubernetes routing.
This keeps a single TLS termination layer (Traefik) and a single DNS entry point (*.home.server via Pi-hole), while letting in-cluster Ingress resources carry the routing logic as code alongside the workloads they protect.
Adding a new app
-
Create the workload manifests.
mkdir kubernetes/apps/<name>Drop your
deployment.yaml,service.yaml,configmap.yaml,ingress.yaml(if any), and akustomization.yamllisting them as resources. Thekustomization.yamlis required — Argo CD uses it as the entry point for thepath. -
Create the Application manifest.
mkdir kubernetes/argocd-apps/<name>Copy
kubernetes/argocd-apps/homepage/homepage-app.yamlas a template intokubernetes/argocd-apps/<name>/<name>-app.yaml. Update the three fields that vary:metadata.name→ the new app's namespec.source.path→kubernetes/apps/<name>spec.destination.namespace→ the namespace the app runs in
-
Apply the Application once.
kubectl apply -f kubernetes/argocd-apps/<name>/<name>-app.yamlArgo CD picks it up immediately and creates the app. After this initial
kubectl apply, all future changes — including edits to the manifests, resource changes, even namespace recreation — flow through git alone. Argo CD'sselfHealandprunewill keep the cluster in sync with whatever is onmain. -
Point Traefik at it. The app's Ingress will typically be served by the in-cluster ingress-nginx behind a NodePort. Add or reuse a Traefik router (Docker labels on the ingress-nginx Service itself, or a
dynamic/file-provider entry if the Service isn't onweb_net) so<name>.home.serverresolves to the right NodePort.
That's it — commit, push, and Argo CD handles the rest. No manual kubectl on the cluster; no drift between what's running and what's in git.
Why this matters
On a single-node learning cluster this matters less for HA and more for discipline: writing a Deployment that actually ships is different from applying one manually and calling it done. GitOps forces the manifests and the running cluster to be the same thing — the diff in the Argo CD UI is an honest report of "what happens if I merge this PR," not a best-effort guess at what kubectl apply did last time.
