Table of Contents
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:
- Infrastruktur (
infrastructue.yaml
) - Apps (
apps.yaml
)
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:
- infrastructure/api01m300.cpu.cafe/controllers/cert-manager.yaml
- infrastructure/api01m300.cpu.cafe/controllers/cert-manager-webhook.yaml
- infrastructure/api01m300.cpu.cafe/configs/cluster-issuers.yaml
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
...