user@homelab:~/docs$ cat gitops.md

CI/CD a GitOps

git push → GitHub Actions → GHCR → Flux → k3s: nasazení bez přístupu do clusteru zvenku

# obsah

Jak nasazení funguje

Tok nasazení: git push → GitHub Actions → GHCR → Flux → k3s → Cloudflare
Kliknutím otevřeš diagram v plné velikosti
  1. Mac: změna v repozitáři, git commit a git push.
  2. GitHub Actions: sestaví image webu, nahraje ho do GHCR (GitHub Container Registry) a zapíše jeho nový tag do k8s/apps/homelab-docs/kustomization.yaml.
  3. Flux v clusteru každou minutu kontroluje repozitář. Nový commit stáhne a aplikuje na cluster.
  4. Kubernetes stáhne nový image ze soukromého GHCR a vymění pod (rolling update bez výpadku).
  5. Cloudflare Tunnel posílá provoz z internetu do Traefiku a ten na web.
FázeTypická doba
build v GitHub Actions (s cache)30–60 s
Flux si všimne nového commitudo 1 min
rolling update podu10–20 s
od git push po web~1,5–3 min

Proč GitOps (pull model)

Push (CI nasazuje do clusteru)Pull (GitOps, Flux)
Kdo mění clusterCI pipeline zvenkuagent uvnitř clusteru
Přístup do clusteru zvenkuCI potřebuje kubeconfig nebo VPNžádný
Otevřené portyčasto anožádné, Flux jen stahuje
Zdroj pravdystav posledního běhu pipelinegit: co je v gitu, to běží
Ruční zásah v clusteruzůstane, dokud ho někdo nevrátíFlux ho při další synchronizaci vrátí podle gitu
Rollbackznovu spustit starší pipelinegit revert

Pro homelab za NAT bez veřejné IP je pull model přirozená volba. GitHub do domácí sítě nevidí a vidět nemusí.

Repozitář

Soukromý repozitář homelab na GitHubu:

text
homelab/
├── .github/workflows/docs-site.yml   # CI: build → GHCR → bump tagu
├── docs-site/                        # Hugo web (CS/EN), Dockerfile, nginx
├── k8s/
│   ├── apps/homelab-docs/            # Namespace, Deployment, Service, Ingress, kustomization
│   └── infra/cloudflared/            # Cloudflare Tunnel konektor
└── clusters/homelab/
    ├── flux-system/                  # komponenty Fluxu (vygeneroval bootstrap)
    ├── infra.yaml                    # Flux Kustomization → ./k8s/infra
    └── apps.yaml                     # Flux Kustomization → ./k8s/apps (dependsOn: infra)

CI: GitHub Actions

Workflow .github/workflows/docs-site.yml se spustí při pushi do main, pokud se změnilo něco v docs-site/ nebo samotný workflow:

KrokCo dělá
Checkoutstáhne repozitář
Compute image name and tagghcr.io/<owner>/homelab-docs, tag YYYYMMDD-<číslo běhu>-<sha>
Log in to GHCRpřihlášení vestavěným GITHUB_TOKEN, žádný vlastní token
Build and pushmulti-stage build (Hugo → nginx), cache ve GitHub Actions; tag a commit jdou jako build args do patičky webu
Bump image in kustomizationsed zapíše nový newName/newTag, commit jako github-actions[bot] a push
  • Tag nese původ: 20261029-2-bd47ef2 = datum, číslo běhu a commit, ze kterého image vznikl.
  • Žádná smyčka: push provedený pomocí GITHUB_TOKEN nespouští další workflow a změna v k8s/ stejně neodpovídá filtru paths.
  • Minimální práva: contents: write (commit tagu) a packages: write (push image), nic víc.
  • Verze akcí: actions/checkout@v5, docker/login-action@v4, docker/setup-buildx-action@v4, docker/build-push-action@v7, všechny na Node.js 24.

Flux

Instalace CLI na server a kontrola předpokladů:

shell @ server
curl -s https://fluxcd.io/install.sh | sudo bash
flux check --pre

Bootstrap nainstaluje Flux do clusteru a propojí ho s repozitářem. Potřebuje jednorázový fine-grained token (jen repozitář homelab, Administration a Contents: Read and write, platnost 7 dní). Token se načte bez zobrazení, řádek s read se spouští samostatně:

shell @ server
read -rsp "GitHub token: " GITHUB_TOKEN; echo
shell @ server
export GITHUB_TOKEN
flux bootstrap github --owner=<github-user> --repository=homelab --branch=main --path=clusters/homelab --personal
unset GITHUB_TOKEN

Bootstrap:

  • nainstaluje kontrolery do flux-system (source, kustomize, helm, notification),
  • vytvoří v repozitáři deploy klíč jen pro čtení, kterým se Flux dál přihlašuje,
  • nahraje vlastní manifesty do clusters/homelab/flux-system/.

Po bootstrapu se token smaže. Na běžný provoz ho Flux nepotřebuje.

Kustomizations

yaml @ git
apiVersion: kustomize.toolkit.fluxcd.io/v1
kind: Kustomization
metadata:
  name: apps
  namespace: flux-system
spec:
  dependsOn:
    - name: infra
  interval: 5m
  retryInterval: 1m
  path: ./k8s/apps
  prune: true
  wait: true
  sourceRef:
    kind: GitRepository
    name: flux-system
  • dependsOn: infra: aplikace se nasadí až po infrastruktuře. Hláška dependency 'flux-system/infra' is not ready po novém commitu je proto normální a za chvíli zmizí.
  • prune: true: co se smaže z gitu, smaže Flux i z clusteru.
  • wait: true: Kustomization je Ready až ve chvíli, kdy jsou všechny objekty zdravé (např. Deployment dokončil rollout).
  • Existující objekty (web, cloudflared) Flux při prvním běhu převzal bez restartu, protože jejich definice v gitu odpovídala stavu v clusteru.

Soukromý registr image

Repozitář i image jsou soukromé. Kubernetes proto image stahuje s tokenem uloženým jako Secret.

  1. GitHub → Developer settings → Personal access tokens → Tokens (classic): scope jen read:packages, platnost 1 rok. GHCR podporuje jen klasické tokeny.
  2. Secret v clusteru (řádek s read samostatně):
shell @ server
read -rsp "GHCR token: " GHCR_TOKEN; echo
shell @ server
kubectl -n homelab-docs create secret docker-registry ghcr-pull \
  --docker-server=ghcr.io --docker-username=<github-user> --docker-password="$GHCR_TOKEN"
unset GHCR_TOKEN
  1. Deployment na Secret odkazuje:
yaml @ git
spec:
  template:
    spec:
      imagePullSecrets:
        - name: ghcr-pull

Kontrola uloženého tokenu bez jeho vypsání:

shell @ server
kubectl -n homelab-docs get secret ghcr-pull -o jsonpath='{.data.\.dockerconfigjson}' | base64 -d \
  | python3 -c 'import json,sys,base64; a=json.load(sys.stdin)["auths"]["ghcr.io"]; u,p=base64.b64decode(a["auth"]).decode().split(":",1); print(u, len(p), p[:4])'

Identita v gitu

Commity v osobním repozitáři mají jinou identitu než pracovní. Git ji vybírá automaticky podle složky:

shell @ mac
git config --file ~/.gitconfig-personal user.name "<jméno>"
git config --file ~/.gitconfig-personal user.email "<id>+<user>@users.noreply.github.com"
git config --global 'includeIf.gitdir:~/Projects/.path' '~/.gitconfig-personal'
  • všechno ve ~/Projects/ používá osobní identitu, pracovní config se nemění,
  • e-mail je noreply adresa GitHubu (Settings → Emails → Keep my email addresses private), takže skutečný e-mail se v historii neobjeví,
  • Block command line pushes that expose my email je pojistka: GitHub odmítne push se skutečným e-mailem.

Kontrola v repozitáři:

shell @ mac
git config --show-origin user.email

Každodenní práce

shell @ mac
cd ~/Projects/homelab
git pull                                   # stáhnout commity od CI a Fluxu
# … úprava obsahu v docs-site/site/content/{en,cs}/ …
git commit -am "docs: …" && git push
gh run watch                               # průběh buildu
shell @ server
flux get kustomizations --watch            # Flux aplikuje novou revizi

Lokální náhled bez nasazení: cd docs-site/site && hugo server → http://localhost:1313

Kontrola a návrat

shell @ server
flux get kustomizations
flux reconcile kustomization apps --with-source   # synchronizovat hned, nečekat na interval
kubectl -n homelab-docs get deploy homelab-docs -o jsonpath='{.spec.template.spec.containers[0].image}{"\n"}'

Návrat k předchozí verzi přes git, aby se změna neztratila při další synchronizaci:

shell @ mac
git log --oneline -- k8s/apps/homelab-docs/kustomization.yaml
git revert <commit-release>
git push

Tajemství

V repozitáři nejsou žádná tajemství. Vznikají jednou ručně přímo v clusteru:

SecretNamespaceK čemuObnova
tunnel-tokencloudflaredCloudflare Tunnelpři úniku Rotate token v Cloudflare
ghcr-pullhomelab-docsstažení soukromého imagejednou ročně (expirace tokenu)
flux-systemflux-systemdeploy klíč Fluxu (jen čtení)vytvořil bootstrap