From 5f75758f8fc2282a166430f207c076ad98af9020 Mon Sep 17 00:00:00 2001 From: rezayaghobi-dev Date: Sun, 20 Sep 2026 12:47:30 +0330 Subject: [PATCH] add argocd documentation --- README.md | 18 ++++- docs/Home.md | 3 +- docs/K3s.md | 19 +++++- docs/argocd.md | 112 +++++++++++++++++++++++++++++++ kubernetes/apps/README.md | 43 ++++++++++++ kubernetes/argocd-apps/README.md | 69 +++++++++++++++++++ 6 files changed, 260 insertions(+), 4 deletions(-) create mode 100644 docs/argocd.md create mode 100644 kubernetes/apps/README.md create mode 100644 kubernetes/argocd-apps/README.md diff --git a/README.md b/README.md index 46ab78e..60e8bb7 100644 --- a/README.md +++ b/README.md @@ -12,6 +12,7 @@ A self-hosted homelab running on Docker Compose, built and maintained on an Ubun ![Kubernetes](https://img.shields.io/badge/Kubernetes-326CE5?style=flat&logo=kubernetes&logoColor=white) ![K3s](https://img.shields.io/badge/K3s-FFC61C?style=flat&logo=k3s&logoColor=white) ![Vagrant](https://img.shields.io/badge/Vagrant-1868F2?style=flat&logo=vagrant&logoColor=white) +![ArgoCD](https://img.shields.io/badge/ArgoCD-EF8B1B?style=flat&logo=argocd&logoColor=white) ![GitLab](https://img.shields.io/badge/GitLab-FC6D26?style=flat&logo=gitlab&logoColor=white) ![Traefik](https://img.shields.io/badge/Traefik-36CFD1?style=flat&logo=traefik&logoColor=white) ![Nexus](https://img.shields.io/badge/Nexus-BC1E2D?style=flat&logo=sonatype&logoColor=white) @@ -46,7 +47,7 @@ This is a real, running homelab — not a demo. It's deployed on a home Ubuntu s | **Custom CI images** | `docker-compose/gitlab/ci-images-reference/` | Self-built, minimal Docker images for CI pipelines, stored in a self-hosted Container Registry, rebuilt biweekly — see [Custom CI Runner Images](docs/CI-Custom-Images.md) | | **Voting app CI/CD** | `voting-app-monorepo` (self-hosted GitLab, referenced here) | Full pipeline: build, test, vulnerability scan, 3-environment deploy with rollback, load test, DB backup to MinIO — see [Voting App — Mono-Repo](docs/Voting-App-Monorepo.md) | | **Object storage** | `docker-compose/minio/` | Self-hosted MinIO (S3-compatible), `-cpuv1` release tag for CPU compatibility — backup target for the voting app's Postgres dumps | -| **K3s cluster** | `kubernetes/` | Single-node K3s cluster running alongside the Docker Compose stack, fronted by the same reverse proxy via NodePort, monitored through the same Prometheus/Grafana/Loki stack — see [K3s](docs/K3s.md) | +| **K3s cluster** | `kubernetes/` | Single-node K3s cluster running alongside the Docker Compose stack, fronted by the same reverse proxy via NodePort on ingress-nginx, managed via **Argo CD** GitOps (applications/ and argocd-apps/ folders) — monitored through the same Prometheus/Grafana/Loki stack — see [K3s](docs/K3s.md) and [ArgoCD](docs/argocd.md) | ## Learning labs (not always-on) @@ -191,6 +192,7 @@ homelab/ │ ├── Architecture-and-Hardware.md │ ├── Monitoring-Stack.md │ ├── K3s.md +│ ├── ArgoCD.md │ ├── Network-Services.md │ ├── Media-Automation.md │ ├── Workflow-Automation.md @@ -209,6 +211,18 @@ homelab/ ├── kubernetes/ │ ├── alloy-rbac/ │ │ └── alloy-rbac.yaml +│ ├── apps/ +│ │ ├── README.md +│ │ └── homepage/ +│ │ ├── configmap.yaml +│ │ ├── deployment.yaml +│ │ ├── ingress.yaml +│ │ ├── kustomization.yaml +│ │ └── service.yaml +│ ├── argocd-apps/ +│ │ ├── README.md +│ │ └── homepage/ +│ │ └── homepage-app.yaml │ ├── kube-state-metrics/ │ │ ├── kube-state-metrics.yaml │ │ └── prometheus-rbac.yaml @@ -245,6 +259,8 @@ docker compose up -d Rather than pulling and recreating manually, every always-on stack has a matching Ansible playbook that pulls the latest images, recreates containers, verifies they're healthy, and prunes unused images — run through Semaphore's web UI. See [Ansible Playbooks](docs/AnsiblePlaybooks.md) for details on how the shared role works. +For the K3s cluster, updates are GitOps-driven rather than Ansible-driven: Kubernetes manifests live in `kubernetes/apps/`, one folder per app with a `kustomization.yaml`, and [Argo CD](docs/argocd.md) continuously syncs the cluster to match whatever is on `main` — no manual `kubectl apply` after the initial setup. New apps are added by dropping the manifests in `kubernetes/apps//` and the corresponding `Application` in `kubernetes/argocd-apps//`. + ## Security notes - All `.env` files, runtime config directories, and databases are excluded via `.gitignore` — only deployment definitions (`docker-compose.yml`) and `.env.example` placeholders are tracked. diff --git a/docs/Home.md b/docs/Home.md index 61a7198..c3e6cb5 100644 --- a/docs/Home.md +++ b/docs/Home.md @@ -8,6 +8,7 @@ Documentation for a self-hosted homelab running entirely on Docker Compose. This - [Monitoring Stack](Monitoring-Stack.md) - [Alerting](Alerting.md) - [K3s](K3s.md) +- [ArgoCD](ArgoCD.md) - [Network Services](Network-Services.md) - [Media Automation](Media-Automation.md) - [Workflow Automation](Workflow-Automation.md) @@ -41,7 +42,7 @@ Documentation for a self-hosted homelab running entirely on Docker Compose. This | **Registry** | **Nexus3** (Docker Hub pull-through cache + artifact repo) | | **Passwords** | **Vaultwarden** (Bitwarden-compatible, 30+ char passwords) | | **CI/CD** | Self-hosted **GitLab** (Omnibus) + Docker-executor **Runner** | -| **Orchestration (K8s)** | Single-node **K3s** cluster, monitored via the same Prometheus/Grafana/Loki stack | +| **Orchestration (K8s)** | Single-node **K3s** cluster, fronted by **Traefik** via ingress-nginx NodePort, managed via **Argo CD** GitOps (manifests in `kubernetes/apps/`, watched by `kubernetes/argocd-apps/`), monitored via the same Prometheus/Grafana/Loki stack | This is a running system, not a demo — screenshots throughout this wiki are from the live dashboards. diff --git a/docs/K3s.md b/docs/K3s.md index 09b7d99..212ef28 100644 --- a/docs/K3s.md +++ b/docs/K3s.md @@ -22,9 +22,24 @@ curl -sfL https://get.k3s.io | INSTALL_K3S_EXEC="server \ - **`--flannel-backend=vxlan`** — explicit rather than relying on the default, and chosen over `host-gw` since VXLAN works across nodes that aren't on the same L2 segment, keeping the door open for a future worker node. - **`--write-kubeconfig-mode=644`** — makes `/etc/rancher/k3s/k3s.yaml` world-readable so a personal, non-root kubeconfig copy can be made without repeated `sudo`. -## Exposure pattern: NPM in front, NodePort behind +## Exposure pattern: Traefik → ingress-nginx → Ingress -Rather than running a second ingress controller inside the cluster (which would mean two reverse proxies stacked for no benefit), each service gets a `Service` of `type: NodePort`. The existing reverse proxy adds one more proxy host pointing at `homeserver:` — identical to how it already fronts every Docker container. From the proxy's perspective, a K8s pod behind a NodePort is indistinguishable from a Docker container; same TLS setup, same cert renewal flow, zero new moving parts in the HTTPS chain. +K3s is fronted by the existing Docker-side Traefik reverse proxy, which already owns ports 80/443 (see [Reverse Proxy](Reverse-Proxy.md)). Because K3s itself can't bind those ports, the current standard for exposing an in-cluster service is: + +``` +Client → Traefik (:80/:443) → ingress-nginx NodePort → Ingress → Service → Pod +``` + +1. **Traefik** (Docker container) terminates TLS (`*.home.server` via mkcert) and routes by `Host` header, same as every Docker Compose service. +2. **ingress-nginx** runs inside K3s. Its `Service` is exposed as a **NodePort** on the host, giving Traefik a stable `homeserver:` target to forward to — no new ports on the K3s side, no port conflicts with the host's Traefik. +3. **ingress-nginx** (the in-cluster Ingress controller) matches the request against `Ingress` resources in the cluster and forwards to the right `Service`. +4. **Service → Pod** as normal Kubernetes routing. + +This keeps a single TLS termination layer 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. Any app deployed via [Argo CD](ArgoCD.md) follows this pattern — its `Ingress` (e.g., `kubernetes/apps/homepage/ingress.yaml`) is served by ingress-nginx behind the NodePort. + +### Legacy NodePort-only services + +The earlier kube-state-metrics deployment was exposed as a standalone `Service` of `type: NodePort` without an in-cluster Ingress — a simpler approach used before ingress-nginx was introduced. Services still using that legacy pattern (direct NodePort, no Ingress object) continue to work the same way: Traefik forwards to `homeserver:` and the Service routes to Pods directly. New work should use the ingress-nginx path above unless a service genuinely has no need for `Host`-based routing. ## Namespace and resource governance diff --git a/docs/argocd.md b/docs/argocd.md new file mode 100644 index 0000000..baff26b --- /dev/null +++ b/docs/argocd.md @@ -0,0 +1,112 @@ +# ArgoCD — GitOps for K3s + +[Argo CD](https://argoproj.github.io/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//`** — the actual Kubernetes workload manifests for each app. Bundled with `kustomization.yaml` (Kustomize), so common operations like renaming a namespace or swapping an image tag can be done via patches rather than editing every YAML. See [`kubernetes/apps/README.md`](../kubernetes/apps/README.md) for details. +- **`kubernetes/argocd-apps//-app.yaml`** — the Argo CD `Application` object that tells Argo CD "watch this git path, deploy it into this namespace." These live one level up, deliberately separate from `kubernetes/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: + +```yaml +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 +``` + +- `path` points at the `kubernetes/apps//` directory — Argo CD treats the `kustomization.yaml` there 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](Traefik.md)). So the exposure pattern for an Argo CD-managed app is: + +``` +Client → Traefik (:443) → ingress-nginx NodePort → Ingress → Service → Pod +``` + +1. **Traefik** (Docker container, `network_mode`-adjacent to the host) terminates TLS (`*.home.server` via mkcert) and routes based on `Host` header. +2. **ingress-nginx** runs *inside* the K3s cluster. Its `Service` is exposed as a **NodePort** on the host, giving Traefik a stable `homeserver:` target to forward to — the same pattern used for pre-ArgoCD K3s workloads (see [K3s — Exposure pattern](K3s.md#Exposure-pattern)). +3. **ingress-nginx** (the in-cluster Ingress controller) receives the request, matches it against `Ingress` resources in the cluster, and forwards to the right `Service`. +4. **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. + +![ArgoCD dashboard](images/argocd.png) + +## Adding a new app + +1. **Create the workload manifests.** + ```bash + mkdir kubernetes/apps/ + ``` + Drop your `deployment.yaml`, `service.yaml`, `configmap.yaml`, `ingress.yaml` (if any), and a `kustomization.yaml` listing them as resources. The `kustomization.yaml` is required — Argo CD uses it as the entry point for the `path`. + +2. **Create the Application manifest.** + ```bash + mkdir kubernetes/argocd-apps/ + ``` + Copy `kubernetes/argocd-apps/homepage/homepage-app.yaml` as a template into `kubernetes/argocd-apps//-app.yaml`. Update the three fields that vary: + - `metadata.name` → the new app's name + - `spec.source.path` → `kubernetes/apps/` + - `spec.destination.namespace` → the namespace the app runs in + +3. **Apply the Application once.** + ```bash + kubectl apply -f kubernetes/argocd-apps//-app.yaml + ``` + Argo 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's `selfHeal` and `prune` will keep the cluster in sync with whatever is on `main`. + +4. **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 on `web_net`) so `.home.server` resolves 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. + +[← Back to Home](Home.md) diff --git a/kubernetes/apps/README.md b/kubernetes/apps/README.md new file mode 100644 index 0000000..a48e1dc --- /dev/null +++ b/kubernetes/apps/README.md @@ -0,0 +1,43 @@ +# Kubernetes Apps + +This folder holds the actual Kubernetes workload manifests for every app managed via Argo CD's GitOps flow. Each subfolder is one application, named after the app itself: + +``` +kubernetes/apps/ +├── README.md ← you are here +├── homepage/ +│ ├── deployment.yaml +│ ├── service.yaml +│ ├── configmap.yaml +│ ├── ingress.yaml +│ └── kustomization.yaml +├── / ← new apps go here +│ ├── ... +│ └── kustomization.yaml +``` + +## What lives here + +- **Workload manifests** — `Deployment`, `Service`, `ConfigMap`, `Ingress`, `Secret`, etc. These describe the pods, networking, and config that Argo CD applies to the cluster. +- **`kustomization.yaml`** — the Kustomize entry point for the app. Argo CD watches the path this file declares, so every app folder must include one and list every sibling manifest under `resources:`. + +These are the files that end up running in the cluster. This is "what runs." + +## What does *not* live here + +- **Argo CD `Application` objects.** Those live in the sibling [`../argocd-apps/`](../argocd-apps/README.md) folder, deliberately kept separate so Argo CD never watches its own definitions. +- **Namespace-scoped governance objects.** The cluster-level `LimitRange` and `ResourceQuota` live in [`../resource-limits/`](../resource-limits/resource-limits.yaml), applied once at cluster setup rather than per-app. + +## Relationship to the rest of the repo + +Every always-on Docker Compose service in this repo (see [`docker-compose/`](../../docker-compose/)) has a self-contained folder with its own `docker-compose.yml`. The Kubernetes side mirrors that convention — one folder per app, fully self-contained — so the structure on both sides is parallel and easy to navigate. + +## Adding a new app + +See [ArgoCD](docs/argocd.md#adding-a-new-app) for the full walkthrough. In short: + +1. Create `kubernetes/apps//` with manifests + `kustomization.yaml`. +2. Create the matching `kubernetes/argocd-apps//-app.yaml` Application manifest pointing at that path. +3. `kubectl apply` the Application once — future changes flow through git. + +[← Back to Home](../../docs/Home.md) diff --git a/kubernetes/argocd-apps/README.md b/kubernetes/argocd-apps/README.md new file mode 100644 index 0000000..2952a7f --- /dev/null +++ b/kubernetes/argocd-apps/README.md @@ -0,0 +1,69 @@ +# Argo CD Application Manifests + +This folder holds the **Argo CD `Application` objects** — the manifests that tell Argo CD which Git path to watch and which cluster/namespace to deploy into. Each subfolder corresponds to one app, matching the name used under [`../apps/`](../apps/README.md): + +``` +kubernetes/argocd-apps/ +├── README.md ← you are here +├── homepage/ +│ └── homepage-app.yaml +└── / + └── -app.yaml +``` + +## What lives here + +- **One `Application` YAML per app**, named `-app.yaml`. This is the Argo CD control plane entry point — it sets the source repo, target revision, git `path`, destination server and namespace, and sync policy (automated with `prune` and `selfHeal`). + +Example (from `homepage/homepage-app.yaml`): + +```yaml +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 +``` + +## What does *not* live here + +- **Workload manifests** (`Deployment`, `Service`, `Ingress`, etc.). Those live in the sibling [`../apps/`](../apps/README.md) folder. This separation is deliberate: Argo CD should never watch its own `Application` definitions, otherwise a recursive sync loop becomes possible. + +## Why this separation matters + +Keeping "what tells Argo CD to run" (`-app.yaml`) and "what actually runs" (the manifests in `../apps//`) in different folders means: + +- **No accidental self-watch.** Argo CD watches `kubernetes/apps/`, not `kubernetes/argocd-apps/`. An Application manifest changing its own `path` would have no effect — it'd be out of the watched tree entirely. +- **Independent lifecycle.** The Application object is applied once via `kubectl`; everything after that — the Deployments, Services, Ingresses it ultimately creates — flows from git through Argo CD's reconciliation loop. +- **One job per file.** Adding an app means adding one file here (the Application) and one directory there (the manifests). Easy to audit, easy to delete. + +## Applying a new Application + +After creating `kubernetes/argocd-apps//-app.yaml`, apply it once: + +```bash +kubectl apply -f kubernetes/argocd-apps//-app.yaml +``` + +Argo CD picks it up immediately, creates the app in its UI, and begins syncing. After this single `kubectl` call, **all** future changes flow through git — modifying the Application manifest itself is rarely needed once it's running. + +## Relationship to the rest of the repo + +The folder structure mirrors [`../apps/`](../apps/README.md) one-to-one: each app under `kubernetes/argocd-apps/` has a matching folder under `kubernetes/apps/`. This isn't strictly enforced by Argo CD, but keeping the two trees parallel makes it trivial to trace "which Application watches which manifests." + +[← Back to Home](../../docs/Home.md)