5 FluxCD
Sangelo edited this page 2025-07-12 09:30:49 +02:00

FluxCD

FluxCD ist ein GitOps Tool, welches automatisiert Manifests und Helm Charts aus diesem Git Repository deployen kann.
Ausserdem kann es Secrets mithilfe von SOPS Managen.

Flux deployed unsere Infrastruktur in zwei Schritten:

Dies wird so organisiert, damit eine Reihenfolge entsteht. Services in der Infrastruktur müssen vor den Services in Apps deployed werden.

Infrastruktur

Die Infrastruktur besteht aus folgenden Services:

  • Helm
    • Longhorn
  • Controllers
    • Cert-Manager Hetzner DNS Webhook
    • Cert-Manager
    • NginX Ingress Controller
  • Configs
    • Cluster-Issuer

Longhorn

Longhorn is unser Storage-Backend, das auf den ganzen Cluster läuft und alle PVCs kontorlliert. Dieses hat einen gesamten nutzbaren Speicher von 70 GB (jeder Server hat 40 GB Speicher, wovon einiges für das System reserviert wird).

Longhorn selber nutzt alle default Helm Values, ausser folgende Konfiguration:

spec:
  values:
    persistence:
      defaultClassReplicaCount: 1

Mit dieser Konfigurationsoption stellen wir sicher, dass Longhorn nur eine Replica der PVCs macht, da unser Speicher nicht für mehr reicht. In einem Produktionscluster würde man diese natürlich höher setzen, der Default Wert ist 3 Replica.

Sobald Longhorn deployed ist, kann man in einem Deployment die storageClass: longhorn referenzieren, um ein PVC auf Longhorn zu erstellen.

Cert-Manager

Cert Manager generiert unsere Zertifikate automatisch auf unserem Cluster. Dafür sind nur drei Datein zuständig:

Diese drei Dateien Deployen cert-manager mit dessen CRDs, cert-manager-webhook-hetzner für die Challenge-Lösung mit DNS im Hetzner DNS, und einen Issuer, der das ganze definiert. Dieser ist der interessanteste:

---
apiVersion: cert-manager.io/v1
kind: ClusterIssuer
metadata:
  name: letsencrypt
spec:
  acme:
    email: letsencrypt@lunivity.com
    # The server is replaced in /clusters/production/infrastructure.yaml
    server: https://acme-staging-v02.api.letsencrypt.org/directory
    privateKeySecretRef:
      name: letsencrypt
    solvers:
      - dns01:
          webhook:
            # This group needs to be configured when installing the helm package, otherwise the webhook won't have permission to create an ACME challenge for this API group.
            groupName: acme.cpu.cafe
            solverName: hetzner
            config:
              secretName: hetzner-secret
              zoneName: cpu.cafe # (Optional): When not provided the Zone will searched in Hetzner API by recursion on full domain name
              apiUrl: https://dns.hetzner.com/api/v1

Der angegebene LetsEncrypt Server ist hier zum testen noch auf dem Staging Server von LetsEncrypt gesetzt, damit wir nicht geratelimited werden beim Testen.
Diese URL wird mit einem Patch aus der dort dokumentierten Datei wieder ersetzt beim Deployment auf den Cluster.

Der Solver von den Challenges ist der vorhin genannte Webhook, der mit der Hetzner DNS API ein TXT Record mit dem Challenge erstellt.
Der API Key der dort refferenziert wird (hetzner-secret), wird über die Cloud-Init Datei erstellt.

# controller-init.yaml.tftpl
...
  - path: /var/lib/rancher/k3s/server/manifests/fluxcd-namespace.yaml
    content: |
      apiVersion: v1
      kind: Namespace
      metadata:
        name: cert-manager
  - path: /var/lib/rancher/k3s/server/manifests/hcloud-csi-secret.yaml
    content: |
      apiVersion: v1
      kind: Secret
      metadata:
        name: hetzner-secret
        namespace: cert-manager
      data:
        api-key: ${hdns_token_b64}
...

Das Token wird noch mit Terraform im .tf-File vom Controller base64-Kodiert, damit das das K8s-Secret format einhält.

Apps

Momentan wird nur Forgejo in den Apps deployed, zusammen mit Forgejo Actions.

Forgejo

Forgejo besteht aus folgenden Elementen:

  • Namespace
  • Helm Release
  • Secrets

Forgejo Konfiguration

Forgejo's Konfiguration ist relativ gross, also dokumentieren wir hier nur die wichtigsten Values:

# ingress
...
    ingress:
      enabled: true
      className: nginx
      annotations:
        cert-manager.io/cluster-issuer: "letsencrypt"
        load-balancer.hetzner.cloud/hostname: "git.m300.cpu.cafe"
        # #---
        ingress.kubernetes.io/ssl-redirect: "true"
        ingress.kubernetes.io/proxy-body-size: "0"
        traefik.ingress.kubernetes.io/router.entrypoints: "websecure"
      hosts:
        - host: git.m300.cpu.cafe
          paths:
            - path: /
              pathType: Prefix
              port: http
      tls:
        - secretName: forgejo-tls
          hosts:
            - git.m300.cpu.cafe
...

Diese Ingress-Definition konfiguriert das Forgejo HTTP-Service so, dass es über den NginX Ingress Controller auf unser Hetzner Load Balancer ins Internet gestellt wird. Parallel dazu sagen wir cert-manager, dass es das Zertifikat mit dem letsencrypt-Issuer generieren und managen soll.

# admin user secret
...
    gitea:
      admin:
        existingSecret: forgejo-admin-secret
        passwordMode: keepUpdated
...

Der Admin-User wird von einem Secret (mehr dazu später) definiert und erstellt. Dieses Secret enthält eine E-Mail, ein Username und ein zufällig generiertes Passwort für den Admin-User.

# forgejo-admin-secret
apiVersion: v1
data:
  email: <base64_string>
  username: <base64_string>
  password: <base64_string>
kind: Secret
metadata:
  ...

Redis und PostgreSQL sind im Helm-Chart für Forgejo bereits enthalten und sind perfekt für ein kleineres Setup wie unseres. Aus zeitlichen Gründen haben wir für diese Backends nur ein Single-Pod Deployment erstellt und keine HA-Cluster, wäre aber alles im gleichen Helm Chart definierbar.

Man kann hier sehen, dass keine Credentials für die Datenbanken angegeben werden. Das ist so, weil die offizielle Dokumentation für das Forgejo Helm Chart das explizit so dokumentiert. Wir haben es bereits mit Secrets probiert, aber Forgejo kriegt diese nicht mit, wenn man es in so einem Setup aufsetzt.
Wenn man eine externe Datenbank nutzen würde, kann man das leicht in den Values hinterlegen, aber nicht für unser Setup.

# datenbanken
...
    redis-cluster:
      enabled: false

    redis:
      enabled: true
      master:
        count: 1

    postgresql-ha:
      enabled: false

    postgresql:
      enabled: true
      global:
        postgresql:
          service:
            ports:
              postgresql: 5432
      primary:
        persistence:
          size: 10Gi
...

Secrets

Secrets werden von FluxCD mithilfe von mozilla-sops gemanaged. Die Secrets werden mit folgendem kubectl Command erstellt, und im Nachhinein mit sops verschlüsselt:

kubectl create secret generic <secret_name> \
  --from-literal=value=key \
  --dry-run=client \
  -o yaml > secret.yaml

Dieses Kubernetes Secret kann dann mit dem GPG-Public-Key (apps/.sops.pub.asc) verschlüsselt werden:

gpg --import apps/.sops.pub.asc
sops -e --in-place secret.yaml

Wenn man das Secret wieder entschlüsseln möchte, kann man folgenden Command ausführen:

sops -d --in-place secret.yaml

Dafür benötigt man aber den GPG-Private-Key, der natürlich nicht auf diesem Repository zu finden ist.

FluxCD kann die Secrets mit diesem Private Key entschlüsseln, da bei der Cluster-Initialisierung mit Cloud-Init der Private Key mit einem Actions-Secret übergeben wird:

# controller-init.yaml.tftpl
...
  - path: /var/lib/rancher/k3s/server/manifests/fluxcd-namespace.yaml
    content: |
      apiVersion: v1
      kind: Namespace
      metadata:
        name: flux-system
  - path: /var/lib/rancher/k3s/server/manifests/fluxcd-sops-private-key.yaml
    content: |
      apiVersion: v1
      kind: Secret
      type: Opaque
      metadata:
        name: sops-gpg
        namespace: flux-system
      data:
        sops.asc: ${sops_gpg_private_key_b64}
...

Der GPG-Key wird hier über den Terraform-Values weitergegeben, und davor noch base64-Kodiert, damit es im richtigen K8s-Secret-Format ist.

Im Apps Kustomization kann nun definiert werden, dass sops-Secrets gehandelt werden sollten:

# clusters/api01m300.cpu.cafe/apps.yaml
...
  decryption:
    provider: sops
    secretRef:
      name: sops-gpg
...