Preview Apps mit GitOps durch Flux und Kubernetes

Im vorherigen Artikel habe ich beschrieben, wie Flux die Ordner im Git Repository überwacht und automatisch den Zustand auf dem Cluster anpasst. Auf dieser Basis ist das Deployen einzelner Branches als Preview relativ einfach möglich.

Teil der Serie
  1. GitOps mit Kubernetes, Kustomize und Flux
  2. Preview Apps mit GitOps durch Flux und Kubernetes

Kustomize link

Durch Hinzufügen einer Kustomize-Datei zu einem Ordner können einzelne Werte im aktuellen Ordner und allen Unterordnern überschrieben werden.

Zu Beginn wird ein Ordner benötigt, in dem sich alle Previews befinden. Im folgenden Beispiel /previews. In diesen Ordner lege ich eine Yaml-Datei (kustomization.yaml), in der später alle Previews registriert werden. Zusätzlich können für alle Previews in Unterordnern Werte gesetzt werden.

Die folgende Datei befindet sich unter /previews/customization.yaml. Weiterer Inhalt wird in der Pipeline generiert.

apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization

Template link

Ich arbeite mit einem Template-Verzeichnis, da Kustomize die Werte nur bedingt überschreiben kann und ich eine einfache Konfiguration haben möchte. Ich lege folgende Datei unter /previews/template/kustomization.yaml an.

apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization

namespace: prv-app-template

resources:
  - namespace.yaml
  - service-url.yaml
  - config.yaml
  - deployment.yaml

images:
  - name: my-frontend-app
    newName: ghcr.io/owner/repository
    newTag: latest

Der namespace wird in der Pipeline überschrieben.

Die resources-Liste muss alle Kubernetes-Dateien enthalten, die mit deployt werden sollen und sich im selben Ordner befinden. Ich werde hier nicht weiter darauf eingehen, da diese bei jedem Deployment unterschiedlich sind.

Die Dateien namespace.yaml und service-url.yaml werden in der unten beschriebenen Pipeline erzeugt. Die Config Map für die Service URL kann einfach im Deployment mit verwendet werden.

Als Image im Deployment muss der Name aus der Kustomization (z.B. my-frontend-app) verwendet werden. Der Image Tag wird in der Pipeline überschrieben.

Pipeline link

In meinem Fall führe ich die Automatisierung in einer GitHub-Pipeline durch. Die Schritte sollten aber auch problemlos in Gitlab funktionieren. Dazu installiere ich Kubectl und Kustomize in der Pipeline. Das mache ich über den Runtime Version Manager asdf.

Verzeichnis anlegen link

INSTANCE_NAME="pr-1234"

cd previews/

# create instance folder if not exists
mkdir -p ${INSTANCE_NAME}

# overwrite files by template
cp -a template/. ${INSTANCE_NAME}/

Preview eintragen link

Mit dem folgenden Befehl wird der Unterordner der Instance in die Datei /previews/kustomization.yaml hinzugefügt.

kustomize edit add resource "${INSTANCE_NAME}"

Die Datei /previews/kustomization.yaml sieht mit dem Instance Name pr-1234 wie folgt aus:

apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
resources:
- pr-1234

Namespace und URL pro Preview link

Jede Preview wird in ihrem eigenen Namespace ausgeführt und die Anwendungen (Laravel oder Phoenix) benötigen oft ihre eigene URL als Config Value.

cd ${INSTANCE_NAME}

NAMESPACE="prv-${INSTANCE_NAME}"
kubectl create namespace "${NAMESPACE}" -o yaml --dry-run=client > namespace.yaml

kustomize edit set namespace "${NAMESPACE}"

Der verwendete Befehl gibt die generierte Ressource als YAML aus und durch den Parameter –dry-run=client wird der Befehl nicht direkt auf dem Cluster ausgeführt. Dadurch werden keine Cluster Zugänge in der Pipeline benötigt und Kubernetes überwacht das Config Repository.

Die URL wird je nach Cluster-Setup unterschiedlich gesetzt. Daher gehe ich hier nur darauf ein, wie die URL einfach der Anwendung mitgeteilt werden kann. Ähnlich wie beim Namespace erstelle ich dazu eine Config Map mit dem Wert der URL. Die Config Map kann ich fest in das Deployment Template eintragen. Andere dynamische Werte können genau so hinterlegt werden.

URL="${INSTANCE_NAME}.prv.company.dev"
kubectl create configmap service-url --from-literal=url_host=${URL} -o yaml --dry-run=client > service-url.yaml

Ein Beispiel für die URL generierung mit KNative durch Annotations wäre folgendes:

kustomize edit add annotation -f "company.dev/domain-environment:prv"
kustomize edit add annotation -f "company.dev/domain-hostname:${INSTANCE_NAME}"

Falls ein Prefix vor allen Resourcen Namen gewünscht ist kann folgende Zeile ausgeführt werden.

kustomize edit set nameprefix "${INSTANCE_NAME}-"

Docker Image link

In der Kustomization (/previews/template/kustomization.yaml) sind die Docker Images vordefiniert. Bei Anwendung der Kustomization werden alle Image-Einstellungen in den Kubernetes-Dateien mit dem Wert image: my-frontend-app überschrieben. Somit ist es auch möglich mehrere verschiedene Images zu verwalten.

images:
  - name: my-frontend-app
    newName: ghcr.io/owner/repository
    newTag: latest

Kustomize hat hier einen sehr praktischen und einfachen Befehl um dies anzupassen.

kustomize edit set image "my-frontend-app=${IMAGE}:${TAG}"

Annotations link

Ich habe einen Service im Cluster laufen, der Kubernetes Events überwacht und an die GitHub Deployment API zurückgibt, ob ein Deployment erfolgreich gestartet wurde. Dazu muss ich Annotations definieren. Es wäre aber auch denkbar, Labels oder Annotations für andere Automatismen im Cluster zu setzen.

kustomize edit add annotation -f "company.dev/repository_owner:company"
kustomize edit add annotation -f "company.dev/repository_name:${REPO}"
kustomize edit add annotation -f "company.dev/deployment_id:${GITHUB_DEPLOYMENT_ID}"

Deploy link

Für das Deployment müssen die geänderten Dateien nur noch in das Repository committet werden. Dies kann auch über PullRequests erfolgen.

TITLE="Deploy ${TAG} to preview/${INSTANCE_NAME} (${DEPLOYMENT_ID})"
BRANCH_NAME="preview-${INSTANCE_NAME}-${GITHUB_DEPLOYMENT_ID}"

cd ../
git checkout -b "${BRANCH_NAME}"
git add .
git commit -m "${TITLE}"
git push --set-upstream origin "${BRANCH_NAME}"

gh pr create \
  --title "${TITLE}" \
  --body "Deploy to ${URL}"

Komplettes Script link

Es ist sehr einfach, das Skript um weitere Variablen zu erweitern. Ich habe es im GitOps Repository als eigene Pipeline laufen. Diese wird von den Projekt Repos über die GitHub API getriggert und bekommt die entsprechenden Felder übergeben. Dadurch entsteht eine synchrone Queue, die automatisch Merge Konflikte verhindert.

Dazu kann das gleiche Skript mit der ein oder anderen if Anweisung auch für Deployments verwendet werden.

INSTANCE_NAME="pr-1234"
GITHUB_DEPLOYMENT_ID="1234567"
IMAGE="ghcr.io/owner/repository"
TAG="sdf1234"

cd previews/

# create instance folder if not exists
mkdir -p ${INSTANCE_NAME}

# overwrite files by template
cp -a template/. ${INSTANCE_NAME}/

# add instance to /previews/kustomization.yaml
kustomize edit add resource "${INSTANCE_NAME}"

# switch to instance
cd ${INSTANCE_NAME}

# create /previews/{instance_name}/namespace.yaml
NAMESPACE="prv-${INSTANCE_NAME}"
kubectl create namespace "${NAMESPACE}" -o yaml --dry-run=client > namespace.yaml

# set namespace at /previews/{instance_name}/kustomization.yaml
kustomize edit set namespace "${NAMESPACE}"

# create configmap with preview url at /previews/{instance_name}/service-url.yaml
URL="${INSTANCE_NAME}.prv.company.dev"
kubectl create configmap service-url --from-literal=url_host=${URL} -o yaml --dry-run=client > service-url.yaml

# add annotations
kustomize edit add annotation -f "company.dev/domain-environment:prv"
kustomize edit add annotation -f "company.dev/domain-hostname:${INSTANCE_NAME}"

# optional prefix all instance resources
kustomize edit set nameprefix "${INSTANCE_NAME}-"

# set image
kustomize edit set image "my-frontend-app=${IMAGE}:${TAG}"

# set annotations to report github deployment state
kustomize edit add annotation -f "company.dev/repository_owner:company"
kustomize edit add annotation -f "company.dev/repository_name:repository"
kustomize edit add annotation -f "company.dev/deployment_id:${GITHUB_DEPLOYMENT_ID}"

# create pull request
TITLE="Deploy ${TAG} to preview/${INSTANCE_NAME} (${DEPLOYMENT_ID})"
BRANCH_NAME="preview-${INSTANCE_NAME}-${GITHUB_DEPLOYMENT_ID}"

cd ../
git checkout -b "${BRANCH_NAME}"
git add .
git commit -m "${TITLE}"
git push --set-upstream origin "${BRANCH_NAME}"

gh pr create \
  --title "${TITLE}" \
  --body "Deploy to ${URL}"