Aus GitHub Actions deployen
Der übliche Weg, aus der CI auf Kubernetes zu deployen: eine Admin-kubeconfig in ein Repository-Secret kopieren und hoffen, dass sie nicht leakt. Dieses Credential ist cluster-admin, lebt jahrelang, wird in Forks und Artefakte kopiert und lässt sich nicht widerrufen, ohne den Cluster neu zu schlüsseln.
Ein besseres Muster funktioniert auf jedem Cluster: eine Deploy-Identität, die einen Namespace ändern darf und sonst nichts, mit einem Token, das abläuft. Ein ServiceAccount bekommt eine Role im Namespace deiner App, du stellst ihm ein Token mit Ablaufdatum aus, und der Workflow schreibt seine kubeconfig zu Beginn jedes Laufs aus drei Werten: der Adresse des API-Servers, seinem CA-Zertifikat und diesem Token. Zum Widerrufen löschst du den ServiceAccount, und jedes Token, das für ihn ausgestellt wurde, funktioniert sofort nicht mehr.
Bei PaaSbox Clusters sind die Kubeconfigs aus dem Portal für eine Person gemacht: Jede ist für einen
Schlüssel versiegelt, den der Browser erzeugt, und läuft nach 10 Minuten bis 24 Stunden ab. Ein
Programm kann sich eine über die REST-API holen, mit einem API-Schlüssel, der den Scope k3s:access hat
(in Arbeit); das Rezept
Ein Testcluster pro Pull Request macht das für
Wegwerf-Cluster. Für einen Cluster, der bleibt, etwa die Produktion, funktioniert das Muster dieser
Anleitung, wie es ist: Du legst den ServiceAccount einmal an, mit einer admin-Kubeconfig aus dem
Portal.
Was du brauchst
Abschnitt betitelt „Was du brauchst“- Einen Cluster, seine Admin-kubeconfig auf deinem Rechner und
kubectl. - Einen Namespace für deine App (unten
myapp) und Manifeste zum Anwenden, zum Beispiel ein kustomize-Overlay. - Einen API-Server, den der Runner erreicht. Erlaubt deine Firewall die Kubernetes-API nur von deinen eigenen Adressen, lass den Job auf einem selbst gehosteten Runner in deinem Netz laufen statt auf denen von GitHub. Auf einem PaaSbox-Cluster ist Port 6443 nur für die Adressbereiche aus den Einstellungen des Clusters offen: Festlegen, wer die API erreicht.
1. Die Deploy-Identität anlegen
Abschnitt betitelt „1. Die Deploy-Identität anlegen“Speichere als deployer.yaml. Die Role listet die Arten, die die Manifeste einer typischen Web-App
enthalten; kürze sie auf deine, und nimm Secrets nur auf, wenn deine Pipeline sie anlegen muss:
apiVersion: v1kind: ServiceAccountmetadata: { name: deployer, namespace: myapp }---apiVersion: rbac.authorization.k8s.io/v1kind: Rolemetadata: { name: deployer, namespace: myapp }rules: - apiGroups: [apps] resources: [deployments] verbs: [get, list, watch, create, update, patch] - apiGroups: [apps] resources: [replicasets] verbs: [get, list, watch] - apiGroups: [""] resources: [services, configmaps] verbs: [get, list, watch, create, update, patch] - apiGroups: [""] resources: [pods, pods/log] verbs: [get, list, watch] - apiGroups: [batch] resources: [jobs] verbs: [get, list, watch, create, delete] - apiGroups: [networking.k8s.io] resources: [ingresses] verbs: [get, list, watch, create, update, patch]---apiVersion: rbac.authorization.k8s.io/v1kind: RoleBindingmetadata: { name: deployer, namespace: myapp }subjects: - { kind: ServiceAccount, name: deployer, namespace: myapp }roleRef: { apiGroup: rbac.authorization.k8s.io, kind: Role, name: deployer }Wende sie mit deiner Admin-kubeconfig an:
kubectl apply -f deployer.yaml2. Ein Token ausstellen und ablegen
Abschnitt betitelt „2. Ein Token ausstellen und ablegen“kubectl -n myapp create token deployer --duration=720hDas fordert beim API-Server ein Token des ServiceAccounts an, das nach 30 Tagen abläuft. Ein
Cluster kann begrenzen, wie lange ein solches Token lebt; tut deiner das, läuft das Token früher
ab. Speichere es als Repository-Secret KUBE_TOKEN (GitHub → Settings → Secrets and variables →
Actions → Secrets).
Adresse und CA-Zertifikat sind nicht geheim. Lies sie aus deiner Admin-kubeconfig und lege sie als
Repository-Variablen KUBE_SERVER und KUBE_CA ab:
kubectl config view --raw --minify -o jsonpath='{.clusters[0].cluster.server}'kubectl config view --raw --minify -o jsonpath='{.clusters[0].cluster.certificate-authority-data}'3. Der Workflow
Abschnitt betitelt „3. Der Workflow“Ein vollständiger Deploy-Workflow: kubeconfig schreiben, Manifeste anwenden, Rollout prüfen. Die
Ubuntu-Runner von GitHub bringen kubectl mit.
name: deployon: push: branches: [main]
jobs: deploy: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4
- name: Write a kubeconfig for this run env: KUBE_SERVER: ${{ vars.KUBE_SERVER }} KUBE_CA: ${{ vars.KUBE_CA }} KUBE_TOKEN: ${{ secrets.KUBE_TOKEN }} run: | export KUBECONFIG="$RUNNER_TEMP/kubeconfig" echo "$KUBE_CA" | base64 -d > "$RUNNER_TEMP/ca.crt" kubectl config set-cluster target --server="$KUBE_SERVER" \ --certificate-authority="$RUNNER_TEMP/ca.crt" --embed-certs kubectl config set-credentials deployer --token="$KUBE_TOKEN" kubectl config set-context deploy --cluster=target --user=deployer --namespace=myapp kubectl config use-context deploy chmod 600 "$KUBECONFIG" echo "KUBECONFIG=$KUBECONFIG" >> "$GITHUB_ENV"
- name: Deploy run: kubectl apply -k deploy/overlays/production
- name: Wait for the rollout run: kubectl rollout status deploy/web --timeout=180sEnthält dein Release einen unveränderlichen Migrations-Job, wie den im Django-SaaS-Guide, kommt derselbe Löschen-dann-anwenden-Schritt zwischen Deploy und Rollout. Die Role oben erlaubt ihn:
- name: Run migrations run: | kubectl delete job myapp-migrate --ignore-not-found kubectl apply -f deploy/overlays/production/migrate-job.yaml kubectl wait --for=condition=complete job/myapp-migrate --timeout=300sWarum das eine Admin-kubeconfig schlägt
Abschnitt betitelt „Warum das eine Admin-kubeconfig schlägt“Was dir dieses Muster bringt, klar benannt:
- Ein Leak bleibt begrenzt. Das Token kann einen Namespace ändern und in keinem anderen etwas lesen: nicht die Secrets einer anderen App, nicht die Nodes des Clusters.
- Ein Leak läuft ab. Das Token funktioniert nach seinem Ablaufdatum nicht mehr. Wähle Wochen, nicht Jahre, und stelle rechtzeitig ein neues aus.
- Widerrufen ist ein Befehl, und er berührt die Schlüssel des Clusters nicht.
kubectl -n myapp delete serviceaccount deployerstoppt sofort jedes Token, das für ihn ausgestellt wurde; wendedeployer.yamlwieder an und stelle ein neues Token aus. - Änderungen sind zuzuordnen. Wo das Audit-Log des API-Servers eingeschaltet ist, trägt jede
Änderung der Pipeline den Namen
system:serviceaccount:myapp:deployer. „Was hat um 02:00 Uhr deployt?“ hat damit eine Antwort.
Weiter gedacht: gar kein gespeichertes Token
Abschnitt betitelt „Weiter gedacht: gar kein gespeichertes Token“GitHub kann jedem Job ein eigenes, kurzlebiges OIDC-Token geben. Ein Kubernetes-API-Server, der über die JWT-Authentifizierung in seiner Authentifizierungskonfiguration dem Aussteller von GitHub vertraut, nimmt dieses Token direkt an, und das Repository speichert gar keinen Cluster-Zugang mehr. Dafür brauchst du Zugriff auf die Flags des API-Servers, den dir eine gemanagte Control Plane womöglich nicht gibt.
Härtungs-Checkliste
Abschnitt betitelt „Härtungs-Checkliste“- Lade die kubeconfig nie als Artefakt hoch und gib sie nie per
catin einem Step aus. Sie enthält das Token. - Ein ServiceAccount pro Pipeline und Namespace (Staging und Produktion getrennt), damit du einen widerrufen kannst, ohne die anderen zu treffen.
- Gib nur frei, was deine Manifeste enthalten. Lass Secrets weg, wenn die Pipeline sie nicht anlegen muss, und binde die Deploy-Identität nie an eine ClusterRole.
- Pinne Image-Tags (
sha-…, nichtlatest), damit die CI genau das deployt, was sie gebaut hat.
Dasselbe Muster funktioniert in jedem CI-System, auch in GitLab CI, Forgejo Actions und Jenkins,
denn es ist nur kubectl: eine beschränkte Identität, ein Token, das abläuft, und eine kubeconfig,
die pro Lauf geschrieben wird.
Nächste Schritte
Abschnitt betitelt „Nächste Schritte“- Ein Testcluster pro Pull Request: ein Wegwerf-Cluster für jeden Pull Request, angelegt und gelöscht von der CI oder einem Agenten (in Arbeit).
- Eine Django-SaaS deployen: die Manifeste und der Migrations-Job, die dieser Workflow anwendet.