Argo CD — GitOps delivery
application.yaml is an Argo CD Application
that continuously syncs the k8s/ kustomization to the cluster, so
every push to main is rolled out automatically.
What it does
- Watches
path: k8sontargetRevision: mainof this repo. - Deploys into the
pixelfluxnamespace (created on first sync). automatedsync withprune: trueandselfHeal: true— the cluster is kept exactly in sync with git; manual changes are reverted, removed manifests are pruned.- Sets the per-cluster ingress host via a kustomize patch in
spec.source.kustomize.patches— the sharedk8s/base stays domain-agnostic (pixelflux.example.com), the real host lives only here.
No secrets needed
Both the repo and the GHCR image are public, so Argo CD clones over anonymous
HTTPS (repoURL: https://github.com/...) and the node pulls the public image —
no deploy key and no pull secret. The images: override in application.yaml
points the app at ghcr.io/vallsp/pixelflux, published by CI on every push to
main.
Automatic image rollout (Argo CD Image Updater)
Argo CD syncs manifests, not images — so a new image under the same tag
wouldn’t otherwise trigger a sync. Argo CD Image
Updater (pinned v0.18.0, the
last classic annotation-driven release) closes that loop. The bring-up script
installs it into the argocd namespace; the tracking lives in annotations on
application.yaml:
- What it watches:
ghcr.io/vallsp/pixelflux:latest(CI pushes:lateston every push tomain). - Strategy
digest: when the:latestdigest changes, it pins the new digest into this Application’s kustomize image override; Argo then rolls the Deployment. No version bump, no re-apply. - Write-back
argocd: it patches this Application via the Kubernetes API. Its install Role grantsapplications: get/list/update/patch, so it needs no Argo CD API token; the public GHCR package means no registry secret.
So the full loop is: push to main → CI builds & pushes :latest → Image
Updater pins the new digest (polls ~every 2 min) → Argo rolls it out.
# Watch it work:
kubectl -n argocd logs deploy/argocd-image-updater -f
kubectl -n argocd get application pixelflux \
-o jsonpath='{.spec.source.kustomize.images}{"\n"}' # flips to a @sha256: ref
Heads-up: re-running
task k3s:up/k3d:upre-appliesapplication.yaml, which briefly resets the override to the bootstrap tag until the next poll re-pins the digest. To follow a versioned tag instead of:latest, switch the strategy tosemverand bumpCargo.toml.
Configuration (no values hard-coded)
The per-cluster settings — your domain and the Let’s Encrypt email — live
in cluster/config.env, never in the manifests. The committed files keep the
pixelflux.example.com / admin@example.com placeholders; the bring-up script
renders your real values in at apply time.
cp cluster/config.env.example cluster/config.env # then edit DOMAIN + ACME_EMAIL
Usage
On a fresh k3s/k3d host, one command installs Argo CD and applies everything with your config:
task k3s:up # VPS / single-node k3s (local Docker: task k3d:up)
If Argo CD is already installed and you’d rather apply by hand, render the domain yourself (the script does this for you otherwise):
DOMAIN=your.domain.com
sed "s/pixelflux\.example\.com/$DOMAIN/g" argocd/application.yaml | kubectl apply -f -
From then on Argo CD reconciles automatically. Manage it with:
kubectl -n argocd get application pixelflux # status
kubectl delete -f argocd/application.yaml # tear down (finalizer cascades)
# pause auto-sync without deleting:
kubectl -n argocd patch application pixelflux --type merge -p '{"spec":{"syncPolicy":null}}'
HTTPS
The synced kustomization carries the HTTPS routing: the HTTP→HTTPS redirect
and the TLS route, with the real host injected onto both routes by the
Application patches. The cluster-level ACME (Let’s Encrypt) resolver lives in
k8s/traefik-acme.yaml and is applied once by the bring-up script with your
ACME_EMAIL — kept out of the GitOps sync so the email stays a config value, not
committed YAML. The certificate is issued automatically on the first request
(~1 min). DNS for the apex and www. must point at the cluster’s IP.
Caveats
- Don’t mix with the manual
task deploy*path.selfHeal/prunewould revert or remove anything you apply by hand on top of what Argo manages. - Separate namespace. Argo CD deploys into
pixelflux; if you previously rantask deploy(default namespace), runtask deploy:downfirst to avoid two copies. - The image must exist. The GHCR image must be published (CI does this on each
push to
main) and the package set to public so the node can pull it and Image Updater can read its tags. :latestis mutable. Image Updater pins a@sha256:digest at runtime, so the running image is deterministic — but the bootstrapimages:tag in git is not. Don’t read the committed tag as “what’s deployed”; check the live Application (see the command above).