GitOps mit Fluxcd

Was ist GitOps?

GitOps setzt sich aus Git und Operations zusammen und beschreibt dass die Infrastruktur (infrastructure as code) selbst auch in einem Git Repository gehostet wird.

Es gibt also bei dem GitOps Verfahren zwei unterschiedliche Repositorys. Ein Repository für den Application Code und ein Repository für die Verwaltung der Infrastruktur in dem die Anwendung laufen soll. Da die Infrastruktur nun auch in einem Git Repository verwaltet wird, lässt sich zu jedem Zeitpunkt und/oder an anderen Orten eine identische Kopie der Anwendung schnell aufbauen. Auch die Nachverfolgbarkeit ist somit gegeben. Bei guter Dokumentation in den Commit Messages, kann jede Änderung auch gut nachvollzogen werden.

Push und Pull

Es gibt für GitOps zwei Varianten wie das realisiert werden kann. Es gibt Push und Pull-basierende Verfahren. Push-basierend bedeutet, dass von außen eine Anwendung die getätigten Änderungen in dem Git Repository im Kubernetes Cluster durchführt. Das klingt erst einmal gut, wenn man aus einer CI/CD Pipeline heraus die Änderungen durchführen kann. Das hat aber einen entscheidenden Nachteil, eine Anwendung von außen hat vollen Zugriff auf den Cluster. Bei dem Pull-basierenden Verfahren geschiet dieses aus dem Cluster heraus und ist daher für den Betrieb also sicherer. Ein Vertreter der Pull-basierenden ist Fluxcd und dieses werden ich hier in diesem Artikel mit einem lokalen Git Repository, welches mit Gitea gehostet wird verwenden, um die Infrastruktur der Anwendung zu beschreiben.

Fluxcd mit lokalen Gitea Host

Nun beschreibe ich wie man GitOps mit Fluxcd umsetzt. Als Werkzeuge hierfür verwende ich K3D, K3S und K9S.

Vorbereitungen

Cluster starten

Für das Demo habe ich einen kleinen Cluster mit

k3d create cluster Demo -w 3 -p 80:80@loadbalancer

erstellt. Damit kubectl und K9S auf den Cluster zugreifen können, müssen wir die Kubeconfig mit K3D mergen mit

k3d get kubeconfig -a

nun greifen die Tools auf den neu erstellten Cluster zu. Hier ein Screenshot von K9S mit dem man einen Kubernetes Cluster schön einfach über die Konsole inspizieren kann.

Alias für Kubectl

Wer es noch nicht eingerichtet hat, der sollte um die Schreibarbeit zu minimieren, ein alias k für kubectl mit

alias k=kubectl

einrichten.

Sollte kubectl noch nicht auf dem System installiert sein, dann kann es einfach mit:

yay -S kubectl

installiert werden.

Kubernetes Namespace

Es ist zu empfehlen, dass fluxcd in einem eigenen Kubernetes Namespace läuft. Ich wähle hierfür flux aus.

k create ns flux

Alternativ kann man den Namespace über ein Kubernetes Manifest flux.namespace.yaml anlegen lassen:

apiVersion: v1
kind: Namespace
metadata:
  name: flux

Dann noch ein kubectl apply mit:

k apply -f flux-namespace.yaml

Erstellung known_hosts Datei

Es gibt 2 Varianten wie auf das Gitea Repository zugegriffen werden kann. Per HTTPS und SSH. Ich habe mich für die SSH Variante entschieden, weil so keine Credentials im Connection String angegeben werden muss und somit auch kein User vorhanden sein muss. Ich verwende SSH, weil so die Deployment Keys verwendet werden und ich der Anwendung fluxcd auf genau ein Repository Zugriff gewähren kann.

Damit per SSH auf das Gitea Repository zugegriffen werden kann, muss in dem fluxcd Kubernetes Deployment eine known_hosts Datei vorhanden sein, da sonst ein Zugriff auf das Gitea Repository nicht möglich ist und somit fluxcd das Repository nicht überwachen kann. Also muss man mit diese mit ssh-keyscan vorab erstellen.

ssh-keyscan gitea.xxx.org > known_hosts    

Achtung: Falls der Git Server auf einem anderen Port lauscht, dann muss dieser mit -p spezifiziert werden. Nur so erhält man die richtigen Host Keys und eine korrekte known_hosts Datei.

Damit später aus dem Deployment von Fluxcd (siehe unten) darauf zugegriffen werden kann, muss die Datei als ConfigMap eingebunden werden.

k create configmap flux-ssh-config --from-file=known_hosts -n flux

Nun ist die ConfigMap in dem Namespace flux als flux-ssh-config bekannt. Darauf werden wir unten zugreifen.

YAML erstellen

Mit fluxctl install wird eine eine Multidatei YAML erzeugt. Diese wird über die Pipe per kubectl apply normalerweise direkt angewendet. Da wir aber für den Betrieb noch ein paar Anpassungen vornehmen müssen, leiten wir die Ausgabe in die Datei fluxcd.yaml um.

Zunächst muss fluxctl auf dem System installiert werden:

yay -S fluxctl

Danach erzeugen wir uns eine initiale YAML, die wir im Anschluss noch modifizieren werden.

fluxctl install --git-user=sascha --git-email=MrPeacock@web.de --git-url="gitea@mrpeacock.duckdns.org:Kubernetes/fluxdemo.git" --git-path=namespaces,workloads --namespace=flux > fluxcd.yaml

Anpassen der fluxcd.yaml

Known_hosts Datei per ConfigMap bereitstellen

# The following volume is for using a customised known_hosts
# file, which you will need to do if you host your own git
# repo rather than using github or the like. You'll also need to
# mount it into the container, below. See
# https://docs.fluxcd.io/en/latest/guides/use-private-git-host.html
- name: ssh-config
  configMap:
    name: flux-ssh-config

Die ConfigMap muss dem Cluster in dem vorher definierten Namespace flux bereitgestellt werden.

 k create configmap flux-ssh-config --from-file=known_hosts -n flux

Jetzt muss die ConfigMap nur noch in den Container unter den Pfad /root/.ssh gemounted werden.

# Include this if you need to mount a customised known_hosts
# file; you'll also need the volume declared above.
- name: ssh-config
  mountPath: /root/.ssh

Nur lese Modus

FluxCD bietet einen nur lese Modus an. Dieses ist praktisch, wenn man die volle Kontrolle über den Cluster behalten möchte. Anderenfalls scannt FluxCD die Imagerepository und aktualisiert per Commit neuere Versionen in den Kubernetes Manifesten.

# Tell flux it has readonly access to the repo (default `false`)
- --git-readonly

Einen Hostname einer festen IP Addresse zuordnen

Falls ein Dienst nicht per Namensauflösung im Container aufgerufen werden kann, dann kann man Hostnames definieren.

#
# map git.xxx.org to 192.168.2.100
#
hostAliases:
  - ip: "192.168.2.1"
    hostnames:
    - "git.xxx.org"

Fertige Datei

---
apiVersion: apps/v1
kind: Deployment
metadata:
  name: flux
  namespace: flux
spec:
  replicas: 1
  selector:
    matchLabels:
      name: flux
  strategy:
    type: Recreate
  template:
    metadata:
      annotations:
        prometheus.io/port: "3031" # tell prometheus to scrape /metrics endpoint's port.
      labels:
        name: flux
    spec:
      nodeSelector:
        beta.kubernetes.io/os: linux
      serviceAccountName: flux
      volumes:
      - name: git-key
        secret:
          secretName: flux-git-deploy
          defaultMode: 0400 # when mounted read-only, we won't be able to chmod

      # This is a tmpfs used for generating SSH keys. In K8s >= 1.10,
      # mounted secrets are read-only, so we need a separate volume we
      # can write to.
      - name: git-keygen
        emptyDir:
          medium: Memory

      # The following volume is for using a customised known_hosts
      # file, which you will need to do if you host your own git
      # repo rather than using github or the like. You'll also need to
      # mount it into the container, below. See
      # https://docs.fluxcd.io/en/latest/guides/use-private-git-host.html
      - name: ssh-config
        configMap:
          name: flux-ssh-config

      # The following volume is for using a customised .kube/config,
      # which you will need to do if you wish to have a different
      # default namespace. You will also need to provide the configmap
      # with an entry for `config`, and uncomment the volumeMount and
      # env entries below.
      # - name: kubeconfig
      #   configMap:
      #     name: flux-kubeconfig

      # The following volume is used to import GPG keys (for signing
      # and verification purposes). You will also need to provide the
      # secret with the keys, and uncomment the volumeMount and args
      # below.
      # - name: gpg-keys
      #   secret:
      #     secretName: flux-gpg-keys
      #     defaultMode: 0400

      #
      # map git.xxx.org to 192.168.2.1
      #
      hostAliases:
        - ip: "192.168.2.1"
          hostnames:
          - "git.xxx.org"

      containers:
      - name: flux
        # There are no ":latest" images for flux. Find the most recent
        # release or image version at https://hub.docker.com/r/fluxcd/flux/tags
        # and replace the tag here.
        image: docker.io/fluxcd/flux:1.18.0
        imagePullPolicy: IfNotPresent
        resources:
          requests:
            cpu: 50m
            memory: 64Mi
        ports:
        - containerPort: 3030 # informational
        livenessProbe:
          httpGet:
            port: 3030
            path: /api/flux/v6/identity.pub
          initialDelaySeconds: 5
          timeoutSeconds: 5
        readinessProbe:
          httpGet:
            port: 3030
            path: /api/flux/v6/identity.pub
          initialDelaySeconds: 5
          timeoutSeconds: 5
        volumeMounts:
        - name: git-key
          mountPath: /etc/fluxd/ssh # to match location given in image's /etc/ssh/config
          readOnly: true # this will be the case perforce in K8s >=1.10
        - name: git-keygen
          mountPath: /var/fluxd/keygen # to match location given in image's /etc/ssh/config

        # Include this if you need to mount a customised known_hosts
        # file; you'll also need the volume declared above.
        - name: ssh-config
          mountPath: /root/.ssh

        # Include this and the volume "kubeconfig" above, and the
        # environment entry "KUBECONFIG" below, to override the config
        # used by kubectl.
        # - name: kubeconfig
        #   mountPath: /etc/fluxd/kube

        # Include this to point kubectl at a different config; you
        # will need to do this if you have mounted an alternate config
        # from a configmap, as in commented blocks above.
        # env:
        # - name: KUBECONFIG
        #   value: /etc/fluxd/kube/config

        # Include this and the volume "gpg-keys" above, and the
        # args below.
        # - name: gpg-keys
        #   mountPath: /root/gpg-import
        #   readOnly: true

        # Include this if you want to supply HTTP basic auth credentials for git
        # via the `GIT_AUTHUSER` and `GIT_AUTHKEY` environment variables using a
        # secret.
        # envFrom:
        # - secretRef:
        #     name: flux-git-auth

        args:

        # If you deployed memcached in a different namespace to flux,
        # or with a different service name, you can supply these
        # following two arguments to tell fluxd how to connect to it.
        # - --memcached-hostname=memcached.default.svc.cluster.local

        # Use the memcached ClusterIP service name by setting the
        # memcached-service to string empty
        - --memcached-service=

        # This must be supplied, and be in the tmpfs (emptyDir)
        # mounted above, for K8s >= 1.10
        - --ssh-keygen-dir=/var/fluxd/keygen

        # Replace the following URL to change the Git repository used by Flux.
        # HTTP basic auth credentials can be supplied using environment variables:
        # https://$(GIT_AUTHUSER):$(GIT_AUTHKEY)@github.com/user/repository.git
        - --git-url=gitea@git.xxx.org:Kubernetes/fluxdemo.git
        - --git-branch=master
        - --git-path=namespaces,workloads
        - --git-label=flux
        - --git-user=sascha
        - --git-email=sascha@edvpfau.de

        # Include these two to enable git commit signing
        # - --git-gpg-key-import=/root/gpg-import
        # - --git-signing-key=<key id>

        # Include this to enable git signature verification
        # - --git-verify-signatures

        # Tell flux it has readonly access to the repo (default `false`)
        - --git-readonly

        # Instruct flux where to put sync bookkeeping (default "git", meaning use a tag in the upstream git repo)
        # - --sync-state=git

        # Include these next two to connect to an "upstream" service
        # (e.g., Weave Cloud). The token is particular to the service.
        # - --connect=wss://cloud.weave.works/api/flux
        # - --token=abc123abc123abc123abc123

        # Enable manifest generation (default `false`)
        # - --manifest-generation=false

        # Serve /metrics endpoint at different port;
        # make sure to set prometheus' annotation to scrape the port value.
        - --listen-metrics=:3031

      # Optional DNS settings, configuring the ndots option may resolve
      # nslookup issues on some Kubernetes setups.
      # dnsPolicy: "None"
      # dnsConfig:
      #   options:
      #     - name: ndots
      #       value: "1"
---
apiVersion: v1
kind: Secret
metadata:
  name: flux-git-deploy
  namespace: flux
type: Opaque
---
# memcached deployment used by Flux to cache
# container image metadata.
apiVersion: apps/v1
kind: Deployment
metadata:
  name: memcached
  namespace: flux
spec:
  replicas: 1
  selector:
    matchLabels:
      name: memcached
  template:
    metadata:
      labels:
        name: memcached
    spec:
      nodeSelector:
        beta.kubernetes.io/os: linux
      containers:
      - name: memcached
        image: memcached:1.5.20
        imagePullPolicy: IfNotPresent
        args:
        - -m 512   # Maximum memory to use, in megabytes
        - -I 5m    # Maximum size for one item
        - -p 11211 # Default port
        # - -vv    # Uncomment to get logs of each request and response.
        ports:
        - name: clients
          containerPort: 11211
        securityContext:
          runAsUser: 11211
          runAsGroup: 11211
          allowPrivilegeEscalation: false
---
apiVersion: v1
kind: Service
metadata:
  name: memcached
  namespace: flux
spec:
  ports:
    - name: memcached
      port: 11211
  selector:
    name: memcached
---
# The service account, cluster roles, and cluster role binding are
# only needed for Kubernetes with role-based access control (RBAC).
apiVersion: v1
kind: ServiceAccount
metadata:
  labels:
    name: flux
  name: flux
  namespace: flux
---
apiVersion: rbac.authorization.k8s.io/v1beta1
kind: ClusterRole
metadata:
  labels:
    name: flux
  name: flux
rules:
  - apiGroups: ['*']
    resources: ['*']
    verbs: ['*']
  - nonResourceURLs: ['*']
    verbs: ['*']
---
apiVersion: rbac.authorization.k8s.io/v1beta1
kind: ClusterRoleBinding
metadata:
  labels:
    name: flux
  name: flux
roleRef:
  apiGroup: rbac.authorization.k8s.io
  kind: ClusterRole
  name: flux
subjects:
  - kind: ServiceAccount
    name: flux
    namespace: flux

Starten

k apply -f ./fluxcd.yaml

Warten bis das Rollout durch ist

k -n flux rollout status deployment/flux

Die Deploy Keys auslesen

Beim Start erstellt fluxcd automatisch eine Identität, mit der dann auf das Git Repository zugegriffen werden kann. Dieses kann man mit in der Logs greppen…

k logs deployment/flux -n flux | grep pub

aber fluxcd kennt mit fluxctl identity eine eigene Anweisung, um die Keys zu extrahieren.

fluxctl identity --k8s-fwd-ns flux

Achtung: Die Deploy Keys werden nur erzeugt, wenn man das SSH Protokoll verwendet! FluxCD kann auch eine Verbindung zu dem Git Repository über eine HTTPS Verbindung aufbauen (siehe oben).

Sync manuell anstoßen

Normalerweise prüft fluxcd alle 5 Minuten das repository auf Änderungen. Gerade für den 1. Test ist das zu lange und daher stoßen wir mit fluxctl sync den Vorgang manuell an.

fluxctl sync --k8s-fwd-ns=flux