Files
homelab-gitops/README.md
T

149 lines
6.6 KiB
Markdown

# Homelab GitOps
Argo CD is the only component that deploys the resources in this repository. Application CI publishes immutable
container images and then commits the selected image tag here; it never receives Kubernetes credentials.
## Repository layout
- `apps/immich-sync/` is the desired Kubernetes state for Immich Share Sync.
- `apps/immichframe/` wraps the reusable ImmichFrame chart with this cluster's
hostname, album selection, resources, and encrypted API credential.
- `apps/metallb/` pins the upstream native MetalLB installation and owns this
cluster's L2 address pool.
- `apps/argocd/` wraps the pinned upstream Argo CD chart with this cluster's
ingress, Authentik OIDC, RBAC, and secret-reference configuration.
- `apps/brgkepler-com/`, `apps/gschissenes-equipment/`,
`apps/immich-crack/`, `apps/pfostendynamics-com/`, and
`apps/watschn-studio/` pin the shared PHP homepage chart and reference their
existing CephFS content claims.
- `bootstrap/` contains Argo CD projects and Applications, including the
upstream Sealed Secrets controller and the five PHP sites above.
- `scripts/update-immich-sync-image.sh` is the single image-promotion interface used by CI.
- `scripts/update-immichframe-chart.sh` is the chart-promotion interface used
by ImmichFrame chart CI.
The only canonical Immich Share Sync image version is `images[0].newTag` in
`apps/immich-sync/kustomization.yaml`. The resource manifest deliberately omits a tag, and Kustomize supplies the
immutable full Git SHA while rendering.
The only canonical ImmichFrame chart version is the `immichframe` dependency
in `apps/immichframe/Chart.yaml`. Chart CI publishes a unique immutable version
and commits that dependency update here. The chart owns the reusable workload
shape; `apps/immichframe/values.yaml` owns only this cluster's choices.
## First-time setup
The cluster already runs Argo CD in the `argocd` namespace and exposes its UI at <https://cd.brunner.ninja>.
Argo CD manages its own chart after bootstrap. If the controller does not
exist, recover it first from the same canonical chart dependency and values,
then recreate the AppProjects and Applications:
```sh
./bootstrap/install-argocd.sh
./bootstrap/install.sh
```
The recovery installer preserves `argocd-secret`, `argocd-oidc`,
`argocd-notifications-secret`, and `argocd-redis`. Back up or provision those
Secrets separately; none of their values belongs in this public repository.
1. Push this repository so Argo CD can read its public HTTPS URL.
2. Configure the `immich-sync` repository's cross-repository token as described below.
3. Review the rendered workload:
```sh
kubectl kustomize apps/immich-sync
```
4. Record the existing persistent volume before the first synchronization:
```sh
kubectl --namespace default get pvc immich-sync-stats \
-o jsonpath='{.metadata.uid}{"|"}{.spec.volumeName}{"\n"}'
```
5. Bootstrap the Argo CD resources:
```sh
./bootstrap/install.sh
```
6. Inspect the result in the UI or CLI:
```sh
argocd app get immich-sync
argocd app diff immich-sync
kubectl --namespace default rollout status deployment/immich-sync
kubectl --namespace default get deployment immich-sync \
-o jsonpath='{.spec.template.spec.containers[0].image}{"\n"}'
```
7. Confirm that the PVC UID and volume name are unchanged. Only after Argo CD reports the application `Synced` and
`Healthy`, remove the obsolete CI deployer and delete the old `KUBE_CONFIG_BASE64` secret from Gitea:
```sh
kubectl --namespace default delete rolebinding/immich-sync-deployer \
role/immich-sync-deployer serviceaccount/immich-sync-deployer \
secret/immich-sync-deployer-token
```
The bootstrap Application enables automatic synchronization, pruning, and self-healing. Consequently, direct edits
to its managed Kubernetes resources are reverted to Git. Change Git when experimenting, observe `OutOfSync`, and then
watch Argo CD return the application to `Synced` and `Healthy`.
## Give Immich Share Sync CI permission to promote images
The built-in Gitea Actions job token cannot write to another repository. Create a dedicated bot account, for example
`gitops-promoter`, add it to `feedc0de/homelab-gitops` with repository **Write** permission, and create a personal
access token for that account with only the `write:repository` scope.
In `feedc0de/immich-sync`, open **Settings → Actions → Secrets** and create `GITOPS_TOKEN` containing that token. The
workflow passes it directly to `actions/checkout` while checking out this repository; the checkout action configures
authenticated HTTPS for the final `git push` without placing the token in a committed URL.
The main-branch workflow now performs this sequence:
1. Build and push `registry.brunner.ninja/feedc0de/immich-sync:<source SHA>`.
2. Verify that the completed build still belongs to the current `main` commit.
3. Update `apps/immich-sync/kustomization.yaml` with that SHA.
4. Commit and push the promotion to this repository.
5. Let Argo CD detect and synchronize the Git commit.
For a personal token owned by `feedc0de`, `write:repository` also applies to other repositories that account can
write. A dedicated bot restricted to this one repository therefore gives the token a meaningfully smaller blast
radius.
## Manual recovery path
Argo CD is the normal deployment mechanism. If it is unavailable, `apps/immich-sync/install.sh` renders, performs a
server-side dry-run, applies the same Kustomize source, and waits for the Deployment rollout. A later Argo CD refresh
will see the same desired resources.
## Sealed Secrets
Argo CD installs the official Sealed Secrets chart pinned in
`bootstrap/sealed-secrets.yaml`. Public Git stores only `SealedSecret`
ciphertext. The controller decrypts that ciphertext into namespace-scoped
Kubernetes Secrets; the private sealing key never belongs in this repository.
Install the matching `kubeseal` client, then seal a Secret for its final name
and namespace. The default strict scope prevents ciphertext from being moved to
another name or namespace:
```sh
kubectl --namespace default create secret generic example \
--from-literal=token='replace-me' \
--dry-run=client --output=yaml \
| kubeseal \
--controller-name sealed-secrets-controller \
--controller-namespace sealed-secrets \
--format yaml > example-sealed-secret.yaml
```
The controller's generated private key is the disaster-recovery key for every
sealed value. Back up the Secrets labelled
`sealedsecrets.bitnami.com/sealed-secrets-key=active` from namespace
`sealed-secrets` to an encrypted offline store. Do not commit that backup. A
cluster rebuilt without one of those keys cannot decrypt existing ciphertext.