Files
k8s-starter-pack/docs/13-blog-gitops.md
T
2026-10-31 20:41:04 +00:00

140 lines
7.1 KiB
Markdown

# 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.