diff --git a/apps/ingress-nginx.yaml b/apps/ingress-nginx.yaml new file mode 100644 index 0000000..145bbda --- /dev/null +++ b/apps/ingress-nginx.yaml @@ -0,0 +1,32 @@ +# ingress-nginx, installed like MetalLB and Longhorn: an Application +# pointing at apps/ingress-nginx, a Helm chart that pulls in the upstream +# ingress-nginx chart. Once this syncs, apps can request a hostname-routed +# address via an Ingress resource instead of each needing its own MetalLB +# LoadBalancer Service. See docs/11-ingress.md. +apiVersion: argoproj.io/v1alpha1 +kind: Application +metadata: + name: ingress-nginx + namespace: argocd + # Safe to cascade: ingress-nginx holds no persistent data of its own, + # unlike apps/longhorn.yaml. + finalizers: + - resources-finalizer.argocd.argoproj.io +spec: + project: default + + source: + repoURL: https://tea.ahmza.com/ahmza/k8s-starter-pack.git + targetRevision: main + path: apps/ingress-nginx + + destination: + server: https://kubernetes.default.svc + namespace: ingress-nginx + + syncPolicy: + automated: + prune: true + selfHeal: true + syncOptions: + - CreateNamespace=true diff --git a/apps/ingress-nginx/Chart.yaml b/apps/ingress-nginx/Chart.yaml new file mode 100644 index 0000000..1fcf915 --- /dev/null +++ b/apps/ingress-nginx/Chart.yaml @@ -0,0 +1,19 @@ +# An "umbrella" chart, same pattern as apps/metallb/ and apps/longhorn/: +# declares a dependency on the upstream ingress-nginx chart. Argo CD detects +# this as a Helm source because a Chart.yaml exists at this path. +# +# See docs/11-ingress.md for why this exists: one shared LoadBalancer +# address routing many hostnames, instead of every app needing its own +# MetalLB address. +apiVersion: v2 +name: ingress-nginx-app +description: Installs ingress-nginx, routing many hostnames through one shared LoadBalancer address +version: 0.1.0 +dependencies: + - name: ingress-nginx + # A version range rather than an exact pin, so this doesn't silently go + # stale. Check https://artifacthub.io/packages/helm/ingress-nginx/ingress-nginx + # for the current release and pin an exact version for a fully + # reproducible install. + version: ">=4.11.0" + repository: https://kubernetes.github.io/ingress-nginx diff --git a/apps/ingress-nginx/values.yaml b/apps/ingress-nginx/values.yaml new file mode 100644 index 0000000..1bd5e56 --- /dev/null +++ b/apps/ingress-nginx/values.yaml @@ -0,0 +1,21 @@ +# Values for the upstream ingress-nginx chart, nested under +# `ingress-nginx:` (the dependency's name in Chart.yaml). +ingress-nginx: + controller: + service: + type: LoadBalancer + annotations: + # Requests this specific address from MetalLB's BGP pool (see + # docs/05-metallb.md) so it stays predictable across rebuilds. + # Must fall inside apps/metallb/templates/ipaddresspool.yaml's + # range, or MetalLB will never satisfy the request. + metallb.io/loadBalancerIPs: "10.10.60.12" + # ingress-nginx's default Pod Security posture (no hostNetwork, no + # hostPath, only the baseline-allowed NET_BIND_SERVICE capability to + # bind ports 80/443) already fits Kubernetes' baseline profile, unlike + # MetalLB or Longhorn. No managedNamespaceMetadata override needed here. + ingressClassResource: + # Every Ingress resource in this cluster that doesn't name a class + # explicitly uses this one automatically \u2014 the same "no + # storageClassName needed" convenience Longhorn gives PVCs. + default: true diff --git a/docs/11-ingress.md b/docs/11-ingress.md new file mode 100644 index 0000000..7324ff9 --- /dev/null +++ b/docs/11-ingress.md @@ -0,0 +1,83 @@ +# 11 — Ingress: many hostnames, one address + +So far, every app that needs to be reachable gets its own dedicated +address from MetalLB's BGP pool (`docs/05-metallb.md`) — Argo CD at +`10.10.60.10`, the demo app at `10.10.60.11`. That's fine for a handful of +apps, but doesn't scale: every app eats another address out of a pool +that's only so big, and there's no way to route by hostname +(`blog.ahmza.com` vs `photos.ahmza.com` sharing one address on port 443). + +## What Ingress actually is + +An **Ingress controller** is a reverse proxy that sits behind **one** +MetalLB address and reads `Ingress` resources — small manifests that say +"requests for this hostname go to that Service." Instead of every app +declaring its own `type: LoadBalancer` Service, most apps go back to a +plain internal `ClusterIP` Service, and get an `Ingress` resource pointing +at it. One address, many hostnames. + +**ingress-nginx** is the controller this cluster uses — the most common, +most documented choice, distributed as a Helm chart just like MetalLB and +Longhorn (see [`apps/ingress-nginx/Chart.yaml`](../apps/ingress-nginx/Chart.yaml)). + +## What's already running + +**`apps/ingress-nginx.yaml`** → one Application, pointing at +`apps/ingress-nginx`, pulling in the upstream chart. +[`apps/ingress-nginx/values.yaml`](../apps/ingress-nginx/values.yaml) gives +its Service a fixed address from the MetalLB pool +(`10.10.60.12` — see [`docs/network.md`](network.md)) and marks its +`IngressClass` as the cluster default, so an `Ingress` resource that +doesn't name a class explicitly still uses it — the same convenience +Longhorn gives PVCs that don't name a `storageClassName`. + +Unlike MetalLB and Longhorn, ingress-nginx needed no Pod Security +exemption: it only needs the `NET_BIND_SERVICE` capability to bind ports +80/443, which Kubernetes' `baseline` profile already permits (unlike +`hostNetwork` or `hostPath`, which it forbids). + +## Verify it's actually running + +```sh +argocd app get ingress-nginx +``` + +```sh +kubectl -n ingress-nginx get svc +``` + +`EXTERNAL-IP` should show `10.10.60.12`. + +```sh +kubectl get ingressclass +``` + +Should show one `IngressClass`, marked as the default via annotation. + +## Routing a real app through it + +Converting an app from its own MetalLB address to Ingress means: + +1. Change its Service from `type: LoadBalancer` back to the implicit + default, `ClusterIP` — it no longer needs its own address. +2. Add an `Ingress` resource naming a real hostname and pointing at that + Service. +3. Point that hostname at ingress-nginx's address, `10.10.60.12` — either + a real DNS record (safe even for a LAN-only cluster: a public DNS + record pointing at a private address just won't resolve to anything + reachable from outside your LAN yet, which is fine until you actually + expose this cluster publicly) or a local override for testing. + +This is exactly what `docs/12-cert-manager.md` builds on next — a real +hostname is what a TLS certificate actually gets issued *for*. + +## Checklist + +- [ ] `ingress-nginx` Application `Synced`/`Healthy` +- [ ] `kubectl -n ingress-nginx get svc` shows `EXTERNAL-IP` `10.10.60.12` +- [ ] `kubectl get ingressclass` shows the default class +- [ ] (Optional, hands-on) Converted one app to route through Ingress with + a real hostname instead of its own MetalLB address + +Continue to [`docs/12-cert-manager.md`](12-cert-manager.md) for real HTTPS +certificates on that hostname. diff --git a/docs/network.md b/docs/network.md index 4cd58a6..453b128 100644 --- a/docs/network.md +++ b/docs/network.md @@ -50,7 +50,7 @@ This dedicated service range is advertised to the MikroTik at |---|---|---|---|---|---| | `10.10.60.10` | Argo CD UI/API (`argocd-server`) | `argocd` | 443 | `https://10.10.60.10` | Patched to `LoadBalancer` by hand — see [`docs/05-metallb.md`](05-metallb.md) | | `10.10.60.11` | Demo app | `demo` | 80 | `http://10.10.60.11` | [`apps/demo/service.yaml`](../apps/demo/service.yaml) | -| `10.10.60.12` | whoami app | `whoami` | 80 | `http://10.10.60.12` | [`apps/whoami/service.yaml`](../apps/whoami/service.yaml) | +| `10.10.60.12` | ingress-nginx (shared, routes by hostname) | `ingress-nginx` | 80, 443 | `http://10.10.60.12`, `https://10.10.60.12` | [`apps/ingress-nginx/values.yaml`](../apps/ingress-nginx/values.yaml) — see [`docs/11-ingress.md`](11-ingress.md) | Add a row here every time you add a new app with a `LoadBalancer` Service (see [`docs/08-adding-new-apps.md`](08-adding-new-apps.md)). Leave the URL @@ -64,3 +64,6 @@ alongside the table above so it's obvious at a glance what's safe to hand to the next app. - `10.10.60.13-10.10.60.250` + +(`whoami`'s former `10.10.60.12` was reclaimed here after that app was +removed as the docs/08 exercise — it now belongs to ingress-nginx instead.)