SaaS-Modernisierung mit Amazon EKS, Teil 3 GitOps-Prozess mit Flux-Werkzeugen aufsetzen

Ein Gastbeitrag von Markus Kokott *

GitOps steht sinnbildlich für einen hohen Automatisierungsgrad, wie er im Software-as-a-Service-, kurz SaaS-Kontext angestrebt wird. Dieser Beitrag zeigt, wie Flux dabei hilft, einen GitOps-Prozess für Änderungen im laufenden Betrieb aufzusetzen.

Firmen zum Thema

GitOps soll in diesem Beispiel einen verbesserten Team-Workflow bei der Bereitstellung einer SaaS-App gewährleisten.
GitOps soll in diesem Beispiel einen verbesserten Team-Workflow bei der Bereitstellung einer SaaS-App gewährleisten.
(Bild: AWS Deutschland)

Dies ist der dritte Teil einer Blog-Post-Reihe zum Thema Software-as-a-Service (SaaS) in Amazon Elastic Kubernetes Service (EKS). In den vorigen Teilen haben wir …

  • das Kubernetes Cluster provisioniert,
  • ein erstes Container Repository in AWS Elastic Container Registry (ECR) angelegt,
  • die SaaS Identität eingeführt, und
  • mit Amazon Cognito einen Identity Provider mit OIDC und JWT Unterstützung eingerichtet.

In der Einführung dieser Reihe haben wir festgestellt, dass ein hoher Automatisierungsgrad und eine Standardisierung der Anwendung über alle Mandanten hinweg für einen effizienten SaaS-Betrieb notwendig ist. GitOps ist in den letzten Jahren im Umfeld von Cloud Native zu einem beliebten Prozess zur Automatisierung von Deployments in Container-Plattformen geworden.

Der Vorteil von GitOps ist dabei, die Stabilität und Verlässlichkeit im Betrieb zu maximieren. Änderungen werden ausschließlich über die Versionskontrolle durchgeführt. Dadurch erhalten wir nicht nur einen Audit-Trail aller Änderungen, wir erhalten auch eine sehr einfache Roll-Back-Funktionalität zur Minimierung der Mean-Time-to-Recovery (MTTR). Wir werden in diesem Beispiel GitOps nur für Änderungen der Anwendung innerhalb eines produktiven Kubernetes-Clusters verwenden.

Weaveworks hat den Begriff GitOps 2017 geprägt. Dabei hat Weaveworks auf der Art aufgebaut, wie Änderungen in Kubernetes durchgeführt werden. Der Zustand des Clusters wird durch die Control-Plane verwaltet. Gewünschte Zustandsänderungen (entweder von außerhalb des Clusters durch Anweisungen über die Kubernetes API oder von innerhalb wie zum Beispiel bei Skalierungs-Events) werden im Key-Value Store etcd persistiert; Controller im Cluster führen kontinuierlich eine Reconciliation durch.

GitOps für Kubernetes führt eine zusätzliche Reconciliation-Schleife ein.
GitOps für Kubernetes führt eine zusätzliche Reconciliation-Schleife ein.
(Bild: AWS Deutschland / GitHub)

Reconcilation bedeutet, dass der Controller den aktuellen Zustand der von ihm überwachten Objekte mit dem gewünschten Zustand in etcd vergleicht und die nötigen Schritte einleitet, um diesen zu erreichen. Dabei ist die Zustandsbeschreibung deklarativ. Im Gegensatz zu einer imperativen Beschreibung wird nicht beschrieben, wie dieser Zustand zu erreichen ist. GitOps für Kubernetes führt eine weitere Reconciliation-Schleife ein: zwischen der Versionskontrolle (in der Regel Git) und dem Key-Value-Store etcd.

Flux v2

Um diese Reconciliation-Schleife zu implementieren, werden wir im Folgenden Flux v2 von Weaveworks verwenden. Flux ist hierbei kein alleinstehendes Werkzeug, das von außen mit der Kubernetes API kommuniziert. Es nutzt viel mehr das Kubernetes-Konzept von Custom Resource Definitions (CRDs).

Eine CRD erweitert die Kubernetes API und ermöglicht es so, den Cluster um Funktionalitäten zu erweitern. Typische Beispiele hierfür sind beispielweise die AWS Controllers for Kubernetes, die AWS Services wie Datenbanken oder Message Broker als native Objekte in den Kubernetes Templates verwalten können. Flux definiert vier Arten von Controllern als CRD:

  • Source Controller überwachen externe Quellen (beispielweise Git oder Helm-Repositories) und erzeugen von anderen Controllern konsumierbare Artefakte im Kubernetes Cluster.
  • Reconciler prüfen die von Source Controllern erzeugten Artefakte und erzeugen bei Bedarf Änderungen im Desired State in etcd, auf den dann wiederum die Runtime Controller von Kubernetes selbst reagieren und den Zustand der Anwendung entsprechend anpassen.
  • Image Automation Controller überwachen den Zustand von Image Repositories, schreiben bei Bedarf neue Image Versionen in die Kubernetes Templates, committen diese an Git Repositories und lösen somit wieder Reconciliation-Schleifen über Source Controllers und Reconcilers aus.
  • Notification Controller interagieren mit externen Systemen via Events und sind beispielweise das Bindeglied zu Slack und somit ChatOps.

Neben den Controllern in Kubernetes benötigen wir noch eine Möglichkeit, den Cluster-Zustand unter Versionskontrolle zu halten. Wie bereits erwähnt, verwenden wir Git – und Flux gibt hier ein paar Konventionen vor. Diese Konventionen sind jedoch nicht starr und erlauben es die Struktur der Repositories auf den jeweiligen Workflow zwischen Development und Betriebsteam anzupassen. Eine Übersicht findet sich in der offiziellen Doku.

Der zu realisierende Team-Workflow.
Der zu realisierende Team-Workflow.
(Bild: AWS Deutschland / GitHub)

Ein häufiges Team-Setup von SaaS Anbietern ist, eine Plattform-Mannschaft für die Entwicklung und Verwaltung des Clusters zu haben, welches das Produktteams dazu befähigt, ihre Anwendung in dieser Plattform zu betreiben. Um die Trennung der Verantwortlichkeiten auch technisch zu gewährleisten, besitzen beide Teams ihre eigenen Git-Repositories. Wir wollen daran angelehnt in diesem Beispiel den vorne zu sehenden Workflow abbilden.

Das Plattformteam verwaltet einen oder mehrere Cluster in seinem Infrastruktur-Repository. Es führt die notwendigen Konfigurationen durch, wie beispielweise die Installation von Flux. Dieser Schritt wird Bootstrapping genannt. Anschließend ist das Team für das Onboarding neuer Mandanten zuständig. Hierzu werden die notwendigen Kubernetes-Templates im Infrastruktur-Repository angelegt.

Im Onboarding-Prozess werden ebenfalls Kubernetes Namespaces und Service Accounts zur Isolierung der Mandanten erzeugt. Außerdem bestimmt der Onboarding-Prozess, welche SaaS-Anwendungen für einen Mandanten provisioniert werden sollen.

Die eigentliche Provisionierung erfolgt dann aber durch Links in die Produkt-Repositories der Produktteams. Hier liegen die Deployment-Manifeste, wie beispielweise Kubernetes Templates oder Helm-Charts. Die Mandanten- oder umgebungsspezifische Konfiguration liegt in unserem Beispiel ebenfalls im Produkt-Repository.

Die Repository Struktur

Für diese Artikelreihe werden wir mit zwei Repositories arbeiten. Das Plattform-Team verwaltet das Infrastruktur-Repository, das den Infrastrukturcode und die Konfiguration des Clusters enthält. Es beinhaltet darüber hinaus noch Konfiguration für die Tenant-Namespaces, in welche die SaaS-Mandanten deployed werden. Hier findet sich der Link zum Produkt Repository des Produkt-Teams. Es enthält das Deployment-Manifest und die App-Konfiguration, auf die im Infrastruktur-Repository verwiesen wird.

Die für diesen Blog Post relevanten Komponenten im Infrastruktur Repository sind:

└─ clusters
   └─ prod-eu
      ├─ flux-system
      │  ├─ # YAML-Dateien für die Flux-Komponenten
      ├─ tenant-001      │  ├─ # 1 Ordner pro Mandant mit YAML-Dateien      └─ tenant-002         └─ # für die SaaS im Mandanten-Namespace

Das Produkt-Repository hingegen enthält das Deployment Manifest, das im Infrastruktur Repository innerhalb des Tenant Unterordners verlinkt ist:

└─ kustomize   └─ # YAML-Files mit dem desired state der SaaS

Scope-Auswahl für das GitHub Token.
Scope-Auswahl für das GitHub Token.
(Bild: AWS Deutschland / GitHub)

Wir verwalten beide Repositories in GitHub und erzeugen uns ein GitHub Token für den Account. Das Token ist eine Alternative zur Passwort-Authentifizierung gegenüber der GitHub API. Wir werden das Token verwenden, um Flux Zugriff auf die Repositories zu geben. Eine detaillierte Anleitung beinhaltet die GitHub-Dokumentation. Das Token muss – wie in der Abbildung zu sehen – alle Berechtigungen des Scope repo enthalten.

Eine Demo App

Unser Beispiel für ein Produkt Repository ist das in der offiziellen Flux Dokumentation verwendete podinfo-Repository von Stefan Prodan von Weaveworks. Im kustomize-Ordner des Repositories ist ein Manifest bestehend aus einem Deployment, einem Service und Konfigurationen zum Skalieren der Pods enthalten:

└─ kustomize
   ├─ deployment.yaml
   ├─ hpa.yaml
   ├─ kustomization.yaml
   └─ service.yaml

Boostrapping des Kubernetes-Cluster

Wir benötigen zwei Kommandozeilen-Tools in unserer lokalen Entwicklungsumgebung, um den oben beschriebenen Workflow mit Flux aufzusetzen. Zunächst muss Flux in Version 2 selbst installiert werden. Das zweite Tool werden wir für das Konfigurationsmanagement einsetzen. Es erzeugt Kubernetes Templates und heißt Kustomize, eine Installationsanleitung findet sich hier.

Nach der Installation exportieren wir unsere GitHub Zugangsdaten in der Kommandozeilen-Session, die wir für das Bootstrapping mit Flux verwenden wollen:

$ export GITHUB_TOKEN=<github-token>
$ export GITHUB_USER=<github-login>

Flux verwendet die kubeconfig zur Authentifizierung am Kubernetes Cluster. Damit dort aktuelle Zugangsinformationen für den EKS Cluster gefunden werden, müssen wir die kubeconfig mit der AWS CLI aktualisieren:

$ aws eks update-kubeconfig --name prod-euAdded new context arn:aws:eks:eu-west-1:<account-id>:cluster/prod-eu to /Users/mkokott/.kube/config

Wir können nun prüfen, ob alle Voraussetzungen erfüllt sind, den Boostrap-Prozess für das Cluster zu starten:

$ flux check --pre► checking prerequisites
✔ kubectl 1.22.2 >=1.18.0-0
✔ Kubernetes 1.20.7-eks-d88609 >=1.16.0-0
✔ prerequisites checks passed

Der Bootstrap-Prozess führt eine Reihe von Schritten im Kubernetes-Cluster und im Infrastruktur-Repository durch:

  • Das Repository wird in GitHub angelegt, falls es nicht bereits existiert.
  • Ein Manifest für die Komponenten von Flux wird im Infrastruktur Repository angelegt.
  • Das Manifest wird verwendet, um die Flux-Komponenten in den Kubernetes Cluster zu deployen.
  • Flux im Kubernetes Cluster wird so konfiguriert, dass der Cluster-Ordner im Infrastruktur Repository auf Änderungen überwacht wird.

Wir starten den Bootstrap-Prozess über die Kommandozeile, in der wir die GitHub Zugangsdaten exportiert haben:

flux bootstrap github \
  --owner=$GITHUB_USER \
  --repository=saas-in-eks-infra \
  --branch=main \
  --path=./clusters/prod-eu \
  --personal
► connecting to github.com
► cloning branch "main" from Git repository "https://github.com/mkokott-aws/saas-in-eks-infra.git"
✔ cloned repository
► generating component manifests
✔ generated component manifests
✔ component manifests are up to date
► installing toolkit.fluxcd.io CRDs
◎ waiting for CRDs to be reconciled
✔ CRDs reconciled successfully
► installing components in "flux-system" namespace
✔ installed components
✔ reconciled components
► determining if source secret "flux-system/flux-system" exists
► generating source secret
✔ configured deploy key "flux-system-main-flux-system-./clusters/prod-eu" for "https://github.com/mkokott-aws/saas-in-eks-infra"
► applying source secret "flux-system/flux-system"
✔ reconciled source secret
► generating sync manifests
✔ generated sync manifests
✔ sync manifests are up to date
► applying sync manifests
✔ reconciled sync configuration
◎ waiting for Kustomization "flux-system/flux-system" to be reconciled
✔ Kustomization reconciled successfully
► confirming components are healthy
✔ source-controller: deployment ready
✔ kustomize-controller: deployment ready
✔ helm-controller: deployment ready
✔ notification-controller: deployment ready
✔ all components are healthy

Im Kubernetes-Cluster existiert nun ein neuer Namespace flux-system, in dem die Controller laufen. Die erste Reconciliation-Schleife wurde ebenfalls eingerichtet. Im neuen Unterordner clusters/prod-eu/flux-system ist in der gotk-sync.yaml-Datei der Cluster-Unterordner im Infrastruktur-Repository registriert:

cat prod-eu/flux-system/gotk-sync.yaml
---
apiVersion: source.toolkit.fluxcd.io/v1beta1
kind: GitRepository
metadata:
  name: flux-system
  namespace: flux-system
spec:
  interval: 1m0s
  ref:
    branch: main
  secretRef:
    name: flux-system
  url: ssh://git@github.com/mkokott-aws/saas-in-eks-infra
---
apiVersion: kustomize.toolkit.fluxcd.io/v1beta1
kind: Kustomization
metadata:
  name: flux-system
  namespace: flux-system
spec:
  interval: 10m0s
  path: ./clusters/prod-eu
  prune: true
  sourceRef:
    kind: GitRepository
    name: flux-system
  validation: client

Hinzufügen neuer Tenants

Diese erste Reconciliation-Schleife ermöglicht es uns nun, über einen Git-Commit neue Mandanten im Cluster zu erzeugen. Dazu arbeiten wir im lokal ausgecheckten Infrastruktur-Repository:

# Erzeugen eines Unterordners für den neuen Tenant
cd clusters/prod-eu/
mkdir my-first-tenant
# jeder Tenant wird in einem eigenen
# Namespace angelegt, für den ein
# eigener Service Account für den
# Reconciliation-Prozess erzeugt wird
flux create tenant my-first-tenant \
  --with-namespace=my-first-tenant \
  --export > rbac.yaml
# eine Reconciliation-Schleife für das
# Produkt Repository wird angelegt
flux create source git my-first-tenant \
  --namespace=my-first-tenant \
  --url="https://github.com/stefanprodan/podinfo.git" \
  --branch=master --export > sync.yaml
# eine Kustomization als Referenz zum
# Deployment-Manifest und die Konfiguration
# der SaaS-App werden hinzugefügt
flux create kustomization my-first-tenant \
  --namespace=my-first-tenant \
  --service-account=my-first-tenant \
  --target-namespace=my-first-tenant \
  --source=GitRepository/my-first-tenant \
  --path=kustomize \
  --export >> sync.yaml
# alle erzeugten YAML-Dateien werden
# in einem Manifest als Kubernetes
# Kustomization Objekt definiert
kustomize create --autodetect
# alle Dateien für den neuen Tenant
# werden ins Infrastruktur-Repository gepusht
git add -A
git commit -m "provisioning my first tenant"
git push

Damit weisen wir Flux an, eine Reconciliation-Schleife für das Produkt-Repository im Namespace des neuen Mandanten anzulegen und das Deployment-Manifest aus dem Unterordner kustomize anzuwenden:

cat sync.yaml
---
apiVersion: source.toolkit.fluxcd.io/v1beta1
kind: GitRepository
metadata:
  name: my-first-tenant
  namespace: my-first-tenant
spec:
  interval: 1m0s
  ref:
    branch: master
  url: https://github.com/stefanprodan/podinfo.git
---
apiVersion: kustomize.toolkit.fluxcd.io/v1beta1
kind: Kustomization
metadata:
  name: my-first-tenant
  namespace: my-first-tenant
spec:
  interval: 1m0s
  path: ./kustomize
  prune: false
  serviceAccountName: my-first-tenant
  sourceRef:
    kind: GitRepository
    name: my-first-tenant
  targetNamespace: my-first-tenant

Der Reconciler nutzt dafür den neu erzeugten Service Account. Dieser erhält über ein RoleBinding administrative Rechte im Namespace:

cat rbac.yaml
---
apiVersion: v1
kind: Namespace
metadata:
  labels:
    toolkit.fluxcd.io/tenant: my-first-tenant
  name: my-first-tenant
---
apiVersion: v1
kind: ServiceAccount
metadata:
  labels:
    toolkit.fluxcd.io/tenant: my-first-tenant
    name: my-first-tenant
    namespace: my-first-tenant
---
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
  labels:
    toolkit.fluxcd.io/tenant: my-first-tenant
  name: my-first-tenant-reconciler
  namespace: my-first-tenant
roleRef:
  apiGroup: rbac.authorization.k8s.io
  kind: ClusterRole
  name: cluster-admin
subjects:
- apiGroup: rbac.authorization.k8s.io
  kind: User
  name: gotk:my-first-tenant:reconciler
- kind: ServiceAccount
  name: my-first-tenant
  namespace: my-first-tenant

Es werden keine Änderungen im Produkt Repository durchgeführt. Der Reconciler benötigt hier lediglich lesenden Zugriff, um die referenzierten Manifestdateien in den Kubernetes Cluster zu übertragen.

Die Reconciliation-Schleife für das Infrastruktur Repository zeigt nach kurzer Zeit die neue Revision. Die mit Flux und Kustomize angelegten Manifeste enthalten die Anweisungen, die App für den neuen Mandanten in einem eigenen Namespace anzulegen:

$ kubectl get namespacesNAME                        STATUS    AGE
default                     Active    5h2m
flux-system                 Active    3h27m
kube-node-lease             Active    5h2m
kube-public                 Active    5h2m
kube-system                 Active    5h2m
my-first-tenant             Active    2m35s
$ kubectl get pods -n my-first-tenantNAME                     READY  STATUS   RESTARTS  AGE
podinfo-96c5c65f6-hg8lc  1/1    Running  0         25s
podinfo-96c5c65f6-nckjk  1/1    Running  0         40s

In diesem Namespace wurde ein zusätzlicher Service Account angelegt, der ein cluster-admin RoleBinding nur in diesem Namespace hat:

$ kubectl get serviceaccounts -n my-first-tenantNAME                 SECRETS   AGE
default              1         2m55s
my-first-tenant      1         2m55s

Der Reconciler verwendet diesen Service-Account, um Änderungen im Deployment-Manifest ausrollen und sämtliche Kubernetes-Objekte in diesem Namespace manipulieren zu können. Außerhalb des eigenen Namensbereichs hat der Reconciler keine Zugriffsrechte. Service-Accounts für Pods zur Laufzeit sind davon nicht betroffen. Sie können weiterhin beliebige Service-Accounts wie zum Beispiel default verwenden.

In diesem Teil der Serie haben wir gesehen, wie ein GitOps-Prozess mit Flux aufgesetzt werden kann. SaaS-Apps für Mandanten werden in dedizierte Namespaces deployed. Die Definition der Deployment-Manifeste liegt in der Hand der Produktteams, das autark vom Plattformteam eine Wartung über Git-Commits betreiben kann.

Das Plattform-Team ist weiterhin für die Kubernetes-Infrastruktur zuständig und hat alle Möglichkeiten zur Isolation der Mandanten. So können zum Beispiel Network Policies verwendet werden, um den Datentransfer zu kontrollieren. Durch die Integration von AWS Identity and Access Management (IAM) in AWS und die Role Based Access Control (RBAC) in Kubernetes kann das Plattform Team auch zentral Zugriffsrechte auf Ressourcen wie zum Beispiel Datenbanken außerhalb des Clusters verwalten.

Markus Kokott
Markus Kokott
(Bild: AWS Deutschland)

Im nächsten Teil der Reihe zeigen wir, wie sich das Onboarding und damit die Provisionierung neuer Mandanten automatisieren lässt.

* Markus Kokott arbeitet als Solutions Architect bei Amazon Web Services. Er hilft Softwareherstellern dabei ihre Prozesse und Produkte zu modernisieren und damit fit für die Cloud zu machen. Technologisch interessiert sich Markus insbesondere für die Bereiche DevOps und Container.

(ID:47826686)