Revert misplaced GitOps files [skip ci]

Reverts ea19586b04, which belonged in ahmza.com/k8s-starter-pack. Restore the blog tree without rewriting history.
This commit is contained in:
ahmza committed 2026-10-31 20:46:33 +00:00
1 parent ea19586b04
commit 06273bfcc8
7 files changed
-409

No files matched your search

-139
View File
@@ -1,139 +0,0 @@
# 13 — Deploy the Hugo blog through Argo CD
The blog source lives in `ahmza/blog-ahmza-com`. This GitOps repository owns
**desired state**: the Kubernetes resources and exact image that should run.
Argo CD compares those files with the cluster and applies changes. CI no longer
needs a kubeconfig, Kubernetes credentials, or a per-job NetBird connection.
The runner still needs working HTTPS access to Gitea and its container registry.
## What owns what
```text
blog source push to main
-> Gitea Actions builds amd64 + arm64 images
-> registry returns the multi-architecture image digest
-> CI commits that digest to this repository
-> app-of-apps discovers apps/blog-ahmza-com.yaml
-> Argo CD applies apps/blog-ahmza-com/ to the cluster
```
A **digest** (`sha256:...`) identifies exact image contents, unlike the mutable
`latest` tag. Both architecture variants are referenced by the same index digest,
so the Pi and x86 workers select their matching image automatically.
The new files are:
- `apps/blog-ahmza-com.yaml`: the Argo CD Application, meaning which Git path to
watch and where to apply it.
- `apps/blog-ahmza-com/namespace.yaml`: the namespace, an isolation boundary for
this application's resources.
- `apps/blog-ahmza-com/deployment.yaml`: two Pods (running container instances),
with zero-unavailable rolling updates and the SWS `/health` probes.
- `apps/blog-ahmza-com/service.yaml`: a ClusterIP Service, a stable internal
destination for those replaceable Pods.
- `apps/blog-ahmza-com/ingress.yaml`: routes HTTPS `blog.ahmza.com` to that Service
through the already-running ingress-nginx at `10.10.60.12`.
There is no PVC or extra Caddy Deployment. The image includes the full generated
site, and TLS is handled by the existing ingress/cert-manager infrastructure.
The Application starts with automatic pruning disabled and no cascading-delete
finalizer. Review deletion behavior before changing either: removing the
Application initially leaves its workloads running rather than deleting them.
## Prepare credentials before enabling the new workflow
In the **blog source repository**, configure these Gitea Actions secrets:
| Secret | Purpose |
|---|---|
| `GITEATOKEN` | Existing credential that can push `ahmza/blog-ahmza-com` registry packages |
| `GITOPS_TOKEN` | Separate bot PAT permitted to read/write `ahmza.com/k8s-starter-pack` |
The bot must be allowed to push to the GitOps repository's `main` branch. Scope
its token and membership as narrowly as Gitea supports; it does not need
Kubernetes credentials or administrator access. Do not commit either token.
At preparation time, the published blog image was readable with an anonymous
registry bearer-token exchange. Therefore no image pull secret is shipped in
these manifests. If package visibility is changed to private, manage a read-only
registry credential in this namespace through the existing SOPS mechanism
(`docs/10-secrets.md`) and reference it using `imagePullSecrets`. The source
repository and registry package have separate visibility settings.
## First deployment: GitOps first, workflow second
1. Review and commit the Application, manifests, and documentation in this
repository, then push to `main`. They must exist remotely before the new CI
workflow can update the Deployment file. Nothing local is visible to Argo CD.
2. The existing app-of-apps watches `apps/` non-recursively. It should create the
child Application without another manual `kubectl apply`. Initial deployment
uses the already-published image index pinned in the manifest. Check:
```sh
kubectl -n argocd get application blog-ahmza-com
kubectl -n blog-ahmza-com get deployment,pods,service,ingress,certificate
kubectl -n blog-ahmza-com rollout status deployment/blog --timeout=5m
```
These commands are diagnostics, not the deployment mechanism. If the child
Application does not appear, inspect `app-of-apps` sync status and conditions.
Do not solve this by letting CI apply resources directly again.
3. Verify the hostname without changing public routing first:
```sh
curl --fail --show-error --resolve blog.ahmza.com:443:10.10.60.12 \
https://blog.ahmza.com/
```
TLS verification remains enabled. If certificate issuance is still pending,
inspect `certificate/blog-tls` and cert-manager events; do not bypass TLS.
Reuse of the ready production DNS-01 issuer follows the other cluster sites.
4. Update external routing only after the new site is healthy. For the existing
NetBird -> ingress1 Caddy -> Kubernetes topology, add `blog.ahmza.com` to the
Caddy matcher forwarding to `10.10.60.12:443`, preserving the hostname for TLS
SNI and HTTP Host. Check that the public NetBird service sends that hostname
to Caddy. Set the LAN's Technitium override to `10.10.60.12` if LAN clients
should use the internal ingress directly. These settings are outside this
repository and have not been changed by adding an Ingress manifest.
Keep public TLS termination, compression and security headers owned by the
intended proxy layer; SWS deliberately has compression/security headers off.
Cloudflare Full/Full (strict), if used, requires HTTPS to Cloudflare's actual
origin. An encrypted NetBird hop later in the path is not a substitute for
that TLS connection.
5. With `GITOPS_TOKEN` configured, commit/push the updated workflow and helper
files in the blog source repository. That push builds the site and updates
only `apps/blog-ahmza-com/deployment.yaml` here to the new digest.
6. Check both the Gitea job and Argo CD. A successful CI job means the image and
desired-state commit were published, NOT that Kubernetes is already healthy.
Argo CD applies the commit asynchronously; inspect its sync/health status and
the Deployment's rollout before declaring the release successful.
7. After a successful cutover, remove obsolete blog repository secrets
`KUBE_CONFIG`, `NETBIRD_SETUP_KEY`, and `NETBIRD_URL` if no other workflow uses
them. If the kubeconfig had broad cluster access, revoke/rotate its credential
separately; deleting a CI secret does not revoke the underlying credential.
Remove no account-wide NetBird policies/keys without checking other users.
## Concurrent publishing and rollback
The blog workflow serializes its publishes. Other applications can still push to
this GitOps repository, so it retries non-fast-forward pushes with a rebase.
Conflicting changes fail visibly instead of force-pushing over someone else's
work. The digest updater refuses missing/duplicate image lines or a different
image repository.
To roll back, revert the GitOps commit changing the blog's image digest, then
push that revert. Keep the old digest in the registry so it remains pullable.
Do not use `kubectl set image`: Argo CD's self-healing would put the Git version
back. A future source build will deliberately publish another desired-state
commit.
> Checkpoint: if an image build succeeds but the GitOps push fails, should Argo
> deploy that image? No: the desired-state commit did not land, so it should keep
> the previous image. This separation is why CI no longer needs cluster access.
-119
View File
@@ -1,119 +0,0 @@
# Network map
Every real IP address this cluster hands out, and exactly what's reachable
there. This file isn't a template with the rest of the repo — it's a log
of *your* actual setup, meant to be updated every time an address is
assigned or changed, so you (or Claude) never have to reverse-engineer
"wait, what's running on .241 again?" from `kubectl` output.
> **Keep this current.** If you're working with Claude and an IP changes —
> a new app gets a `LoadBalancer` address, the MetalLB pool is resized,
> Argo CD's address changes — ask it to update this file, or just point it
> here after any networking change. See `CLAUDE.md` for the standing
> instruction that tells Claude to do this on its own when it makes such a
> change itself.
## The five nodes and the VIP
None of these are from the MetalLB pool below — they're the machines
themselves and the shared address Talos elects between the three control-plane
nodes. Set in [`talos/README.md`](../talos/README.md); see
[`docs/02-talos-bootstrap.md`](02-talos-bootstrap.md) for what the VIP is
and why it's separate from any one node's address. Port 6443 is the Kubernetes
API on control-plane nodes; port 50000 is Talos's TLS-protected management API
on every node.
| Address | What | Port(s) | URL(s) |
|---|---|---|---|
| `10.10.10.200` | `talos-455-i3b` — control plane (Raspberry Pi 4) | 6443, 50000 | `https://10.10.10.200:6443`, `https://10.10.10.200:50000` |
| `10.10.10.201` | `talos-9xy-iuk` — control plane (Raspberry Pi 4) | 6443, 50000 | `https://10.10.10.201:6443`, `https://10.10.10.201:50000` |
| `10.10.10.202` | `talos-k1b-7j5` — control plane (Raspberry Pi 4) | 6443, 50000 | `https://10.10.10.202:6443`, `https://10.10.10.202:50000` |
| `10.10.10.203` | `talos-rss-v5l` — worker01 (Proxmox VM on Gimli) | 50000 | `https://10.10.10.203:50000` |
| `10.10.10.204` | `talos-vba-ood` — worker02 (Proxmox VM on Titus) | 50000 | `https://10.10.10.204:50000` |
| `10.10.10.210` | Cluster VIP (use this one, not a node's own address) | 6443 | `https://10.10.10.210:6443` |
## NetBird overlay
These addresses exist only inside the private NetBird overlay and are subject
to NetBird access policies. All Talos nodes use the official
`siderolabs/netbird` system extension.
| Address | What | Port(s) | URL(s) |
|---|---|---|---|
| `100.71.24.9` | `prd-vps-hel-01` — external Grafana and VictoriaMetrics host; internal name `prd-vps-hel-01.ahmza.com` (authoritative zone in Technitium) | 3000, 8428 | `http://prd-vps-hel-01.ahmza.com:3000`, `http://prd-vps-hel-01.ahmza.com:8428` |
| `100.71.236.121` | `ingress1` — Caddy HTTP origin gateway used by NetBird Reverse Proxy; the WireGuard overlay encrypts this leg | 80 | `http://100.71.236.121` |
| `100.71.121.67` | `talos-455-i3b` — control plane NetBird address | 50000 | `https://100.71.121.67:50000` |
| `100.71.157.217` | `talos-9xy-iuk` — control plane NetBird address | 50000 | `https://100.71.157.217:50000` |
| `100.71.242.40` | `talos-k1b-7j5` — control plane NetBird address | 50000 | `https://100.71.242.40:50000` |
| `100.71.3.210` | `talos-rss-v5l` — worker01 NetBird address | 50000 | `https://100.71.3.210:50000` |
| `100.71.29.51` | `talos-vba-ood` — worker02 NetBird address | 50000 | `https://100.71.29.51:50000` |
## MetalLB pool
This dedicated service range is advertised to the MikroTik at
`10.10.10.1` with BGP by the two workers; it is not an L2 or DHCP pool.
| | |
|---|---|
| Range | `10.10.60.10-10.10.60.250` |
| Router / ASN | MikroTik RB5009 `10.10.10.1` / `64512` |
| Worker ASN | `64520` |
| Configured in | [`apps/metallb/templates/`](../apps/metallb/templates/) |
## Allocated addresses (from the MetalLB pool)
| Address | What | Namespace | Port(s) | URL | Source |
|---|---|---|---|---|---|
| `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` | Reserved for the Authentik LDAP Outpost used by Jellyfin | `authentik` | 636 | `ldaps://10.10.60.11:636` | Planned Authentik deployment; verify the address and TLS certificate before migrating Jellyfin |
| `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 its own dedicated
`LoadBalancer` Service (see [`docs/08-adding-new-apps.md`](08-adding-new-apps.md)).
Most apps should route through ingress-nginx by hostname instead — see the
table below — and only need a dedicated address if they can't speak HTTP
(a raw TCP service, for example). Leave the URL column's port number off
only when it's the protocol default (80 for `http://`, 443 for `https://`)
— include it otherwise.
## Hostnames routed through ingress-nginx
Every real hostname routed to ingress-nginx's shared address
(`10.10.60.12`), and which app it reaches. Public DNS is hosted on
Cloudflare; internal overrides are served by Technitium where noted.
| Hostname | App | Namespace | TLS | Source |
|---|---|---|---|---|
| `demo.ahmza.com` | Demo app | `demo` | Yes — `letsencrypt-prod`, real trusted cert (see [`docs/12-cert-manager.md`](12-cert-manager.md)) | [`apps/demo/ingress.yaml`](../apps/demo/ingress.yaml) |
| `auth.ahmza.com` | Authentik identity provider | `authentik` | Yes — `letsencrypt-prod`, trusted certificate and browser login verified; Technitium routes LAN clients to `10.10.60.12`. For public traffic, NetBird terminates public TLS and proxies HTTP through its encrypted WireGuard overlay to Caddy on `ingress1`; Caddy then uses certificate-validated HTTPS with `auth.ahmza.com` as SNI and the HTTP `Host` when connecting to ingress-nginx at `10.10.60.12:443` | [`apps/authentik/values.yaml`](../apps/authentik/values.yaml) |
| `mc.ahmza.com` | Packwiz modpack files | `mcpacks` | Yes — `letsencrypt-prod`, real trusted certificate | [`apps/mcpacks/ingress.yaml`](../apps/mcpacks/ingress.yaml) |
| `bazarr.ahmza.com` | Bazarr subtitle manager | `bazarr` | Yes — `letsencrypt-prod`, real trusted certificate; Technitium overrides Cloudflare's NetBird route on the LAN | [`apps/bazarr/ingress.yaml`](../apps/bazarr/ingress.yaml) |
| `soulsync.ahmza.com` | SoulSync media discovery | `soulsync` | Configured for `letsencrypt-prod`; verify the certificate after the first deployment; Technitium routes LAN clients to `10.10.60.12` | [`apps/soulsync/ingress.yaml`](../apps/soulsync/ingress.yaml) |
Add a row here every time you point a new hostname at this cluster (see
[`docs/11-ingress.md`](11-ingress.md)). The DNS record itself lives in
Cloudflare, not in this repo — this table is just the log of what's real.
## Planned hostname migrations (not yet verified live)
- `https://blog.ahmza.com` → the `blog` Service in `blog-ahmza-com`, through
the existing ingress-nginx address `https://10.10.60.12`. Defined in
[`apps/blog-ahmza-com/ingress.yaml`](../apps/blog-ahmza-com/ingress.yaml).
Uses `letsencrypt-prod`; verify certificate issuance and update internal
DNS/external Caddy routing before cutover. No new MetalLB address is allocated.
Move this entry into the live hostname table only after verification. See
[`docs/13-blog-gitops.md`](13-blog-gitops.md) for the migration checklist.
## Free in the pool
Addresses from the range above that aren't spoken for yet. Update this
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.
`demo`'s former `10.10.60.11` was reclaimed the same way after that app
moved to routing through ingress-nginx by hostname instead — see
docs/11-ingress.md.)