Neuigkeiten von trion.
Immer gut informiert.

Kubernetes continuous Integration (CI)

Kubernetes

In diesem Kubernetes Beitrag geht es darum, eine CI Pipeline auf Kubernetes Infrastruktur aufzusetzen. Wie bereits in den vorherigen Kubernetes Beiträgen soll auch hier ein Augenmerk auf dem Support der ARM Plattform gelegt werden. Leider trotz der zunehmenden Verbreitung von ARM im Serverumfeld noch immer nicht selbstverständlich, dass Multi-Arch Images bereitgestellt werden.

Kubernetes CI Werkzeuge

Grundsätzlich werden für eine CI-Pipeline zwei Dinge benötigt:

  • Build Server (CI Server)

  • Quellcodeverwaltung (SCM)

Als Versionskontrollsystem (SCM) ist git der Industriestandard, als Repository-Manager ist die Wahl auf gitea gefallen. Bei gitea handelt es sich um einen sehr leichtgewichtigen git-Service, der sich dank HTTP-Webhooks gut mit CI Servern integrieren lässt. Weitere Informationen zu gitea finden sich auf der Homepage: https://gitea.io/

Als CI Server wird Drone verwendet. Ähnlich, wie bei gitea, handelt es sich bei Drone um einen sehr leichtgewichtigen CI Server. Dank dem neu in Drone aufgenommenen Support für Kubernetes, müssen keine extra Build-Agents oder ähnliches konfiguriert werden. Weitere Informationen zu Drone finden sich auf der Homepage: https://drone.io/

Spricht etwas gegen Jenkins? Im Prinzip nicht, das Setup sollte für diesen Beitrag lediglich möglichst einfach gehalten werden. Ein vorbereitetes Jenkins Image mit Docker Client findet sich z.B. hier: https://hub.docker.com/r/trion/jenkins-docker-client

gitea in Kubernetes einrichten

Das Deployment von gitea ist im Prinzip einfach, da vorgefertigte Docker Images bereitstehen. Zur besseren Übersicht wird für gitea, und später auch für drone, ein eigener Namespace eingerichtet.

Kubernetes Namespace für gitea
---
kind: Namespace
apiVersion: v1
metadata:
  name: gitea
  labels:
    name: gitea

Wie üblich kann das Manifest dann über das Kubernetes Dashboard oder mit kubectl apply -f <file> angewendet werden. Um die Resourcen wieder zu löschen, wird kubectl delete -f <file> verwendet.

Beispiel für kubectl Aufruf
$ kubectl apply -f gitea.yml
namespace/gitea created

Damit gitea dauerhaften Speicher zur Verfügung hat, wird ein PersistentVolumeClaim erstellt. Später wird das Volume an den Pod gebunden, um den Speicher zuzuordnen.

Beispiel für gitea PersistentVolumeClaim in Kubernetes
---
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  name: gitea-pv-claim
  namespace: gitea
spec:
  accessModes:
  - ReadWriteMany
  storageClassName: standard
  resources:
    requests:
      storage: 2Gi

Für gitea gibt es leider noch keine Docker Multi-Arch Images, auch wenn das Ticket schon seit 2016 existiert: https://github.com/go-gitea/gitea/issues/531 Daher müssen ARM bzw. ARM64 Nutzer derzeit auf das inoffizielle kunde21/gitea-arm Image ausweichen, oder selber ein Image bauen.

Beispiel für gitea als StatefulSet in Kubernetes auf ARM64
---
apiVersion: apps/v1
kind: StatefulSet
metadata:
  name: gitea-deployment
  namespace: gitea
spec:
  replicas: 1
  serviceName: gitea
  selector:
    matchLabels:
      app: gitea
  template:
    metadata:
      labels:
        app: gitea
    spec:
      containers:
      - name: gitea-container
        #image: gitea/gitea:latest
        image: kunde21/gitea-arm
        imagePullPolicy: Always
        env:
          - name: "ROOT_URL"
            #value: "http://gitea.192.168.99.100.xip.io/"
            #used in webhook notifications, initial redirects
        ports:
        - containerPort: 3000
          protocol: TCP
        volumeMounts:
        - name: gitea-data
          mountPath: /data
          subPath: gitea
      volumes:
      - name: gitea-data
        persistentVolumeClaim:
          claimName: gitea-pv-claim

Damit der Zugriff innerhalb von Kubernetes funktioniert, wird der Pod als Service bereitgestellt. Andere Dienste in Kubernetes erreichen anschließend das gitea über gitea, sofern sie im selben Namespace liegen, bzw. gitea.gitea, wenn der Zugriff aus einem anderen Namespace erfolgt.

Beispiel für gitea Service
---
kind: Service
apiVersion: v1
metadata:
 name: gitea
 namespace: gitea
spec:
  ports:
  - protocol: TCP
    port: 3000
    targetPort: 3000
  selector:
    app: gitea
  type: ClusterIP

Um auch von außend auf den Dienst zugreifen zu können, muss dieser entweder via LoadBalancer oder Ingress verfügbar gemacht werden. Für diesen Beitrag wird traefik als Kubernetes Ingress verwendet.

Beispiel für Ingress auf gitea Service mit traefik
---
kind: Ingress
apiVersion: extensions/v1beta1
metadata:
  name: gitea-ingress
  namespace: gitea
  annotations:
    kubernetes.io/ingress.class: traefik
spec:
  rules:
  # - host: gitea.192.168.99.100.xip.io
  #adjust for your hostname!
# end:ingress[]
  - host: gitea.c2.cloud.sforce.org
    http:
      paths:
      - backend:
          serviceName: gitea
          servicePort: 3000
# end:ingress[]
---
kind: Service
apiVersion: v1
metadata:
  name: gitea-nodeport
  namespace: gitea
spec:
  selector:
    app: gitea
  ports:
  - protocol: TCP
    port: 3000
    nodePort: 30000
    targetPort: 3000
  type: NodePort
# end:nodeport[]

Damit der Zugriff funktioniert, muss bei der Stelle für host ein passender Hostname eingetragen werden. Wird minikube verwendet, so kann z.B. die xip.io Domain verwendet werden, um einen auf das Minikube verweisenden Hostnamen für die lokale IP zu erhalten.
Dazu passend muss die ROOT_URL als Umgebungsparameter gesetzt werden.

Alternativ zu einem Ingress kann der Zugriff über einen NodePort erfolgen, dann wird der Port auf allen Knoten im Cluster exponiert.

Beispiel für NodePort gitea Service
---
kind: Service
apiVersion: v1
metadata:
  name: gitea-nodeport
  namespace: gitea
spec:
  selector:
    app: gitea
  ports:
  - protocol: TCP
    port: 3000
    nodePort: 30000
    targetPort: 3000
  type: NodePort
# end:nodeport[]

Nun kann die Einrichtung von gitea selbst erfolgen.

gitea Setup

Um gitea einzurichten, wird im folgenden die Weboberfläche verwendet. Nach dem Aufruf mit einem Webbrowser klickt man auf "Register" oben links. Anschließend gelangt man zur Einrichtungsoberfläche von gitea.

Für das einfache Setup sind folgende Einstellungen sind anzupassen:

  • Database Type: SQLite3

  • Path: /data/gitea.db

  • Gitea Base URL: http://<ingress host>

Auch hier muss die URL so eingestellt werden, dass ein Zugriff von innerhalb wie außerhalb des Kubernetes Clusters möglich ist.

Am Ende der Liste sollte noch "Administrator Account Settings" ausgewählt werden, und ein Administrator konfiguriert werden. Dabei ist zu beachten, dass der Nutzername nicht "admin" sein darf. Alternativ kann zum Beispiel "master" verwendet werden.

Nachdem das Setup durchlaufen ist, können Repositories angelegt und dann regulär verwendet werden Die Verwendung von bestehenden git Repositories wird ebenfalls unterstützt, um diese entweder zu migrieren oder gitea als Mirror zu verwenden.

Ist git eingerichtet, kann Drone als CI Server eingerichtet werden.

Einrichtung Drone in Kubernetes

Die Einrichtung von Drone CI erfolgt ähnlich zu gitea. Auch hier wird ein eigener Namespace gewählt, um die Übersichtlichkeit zu erhöhen. Aktuell ist ein Multi-Arch Image in Arbeit, jedoch lediglich als Release-Candidate verfügbar. Zusammen mit dem 1.0.0 Release werden dann alle relevanten Plattformen direkt unterstützt.

Falls ARM 64 als Plattform verwendet wird, kann schon jetzt auf drone/drone:1.0.0-rc.4 zurückgegriffen werden.

Beispiel für Drone in Kubernetes
---
kind: Namespace
apiVersion: v1
metadata:
 name: drone
 labels:
   name: drone
---
apiVersion: apps/v1
kind: Deployment
metadata:
 name: server
 namespace: drone
spec:
 replicas: 1
 selector:
   matchLabels:
     name: server
 template:
   metadata:
     labels:
       name: server
   spec:
     containers:
     - name: server
       # image: drone/drone:latest
       image: drone/drone:1.0.0-rc.4
       imagePullPolicy: Always
       env:
         - name: "DRONE_KUBERNETES_ENABLED"
           value: "true"
           #use kubernetes job execution runtime
         - name: "DRONE_KUBERNETES_NAMESPACE"
           value: "drone"
         - name: "DRONE_OPEN"
           value: "true"
         - name: "DRONE_RPC_SECRET"
           value: "dronesecret"
         - name: "DRONE_SERVER_HOST"
           value: "drone"
         - name: "DRONE_SERVER_PROTO"
           value: "http"
         - name: "DRONE_GITEA_SERVER"
           #value: "http://gitea.192.168.99.100.xip.io/"
         - name: "DRONE_GITEA_SKIP_VERIFY"
           value: "true"
         - name: "DRONE_GITEA_PRIVATE_MODE"
           value: "false"
       ports:
         - containerPort: 8000
       volumeMounts:
       - mountPath: /var/lib/drone
         name: drone-lib
     volumes:
     - name: drone-lib
       emptyDir: {}
---
apiVersion: v1
kind: Service
metadata:
  name: drone
  namespace: drone
  labels:
    name: server
spec:
  type: ClusterIP
  ports:
    - name: http
      protocol: TCP
      port: 80
      targetPort: 80
    - name: https
      protocol: TCP
      port: 443
      targetPort: 443
    - name: grpc
      protocol: TCP
      port: 9000
      targetPort: 9000
  selector:
    name: server
---
kind: Ingress
apiVersion: extensions/v1beta1
metadata:
  name: drone-ingress
  namespace: drone
  annotations:
    kubernetes.io/ingress.class: traefik
spec:
  rules:
  # - host: drone.192.168.99.100.xip.io
  #adjust for your hostname!
# end:drone[]
  - host: drone.c2.cloud.sforce.org
    http:
      paths:
      - backend:
          serviceName: server
          servicePort: 80
---
kind: Service
apiVersion: v1
metadata:
  name: gitea
  namespace: drone
spec:
  type: ExternalName
  externalName: gitea.gitea.svc.cluster.local
---
#reverse alias for webhook
kind: Service
apiVersion: v1
metadata:
  name: drone
  namespace: gitea
spec:
  type: ExternalName
  externalName: server.drone.svc.cluster.local
---
kind: Service
apiVersion: v1
metadata:
  name: registry
  namespace: drone
spec:
  type: ExternalName
  externalName: registry.docker-registry.svc.cluster.local
---
apiVersion: rbac.authorization.k8s.io/v1beta1
kind: ClusterRoleBinding
metadata:
  name: drone-rbac
subjects:
  - kind: ServiceAccount
    # Reference to upper's `metadata.name`
    name: default
    # Reference to upper's `metadata.namespace`
    namespace: drone
roleRef:
  kind: ClusterRole
  name: cluster-admin
  apiGroup: rbac.authorization.k8s.io

Nachdem Drone eingerichtet ist, kann auch hier mit dem Webbrowser zugegriffen werden. Als Authentifizierung nutzt Drone den konfigurierten git-Server, also gitea in diesem Beispiel.

Nach einem Login können die Repositories, die mit Drone verwendet werden sollen, ausgewählt werden. Bei jedem Push auf das Repository wird Drone dann von gitea benachrichtigt und kann entsprechende Aktionen starten. Die Konfiguration der auszuführenden Aktionen wird in einer Datei, die standardmäßig unter .drone.yml erwartet wird, im Repository hinterlegt.

Beispiel Drone Pipeline in Kubernetes

Eine Beispielpipeline für Drone in Kubernetes rundet die Einführung ab. Das Beispiel ist bereits auf Kubernetes als Umgebung für die Build Ausführung abgestimmt: Als zusätzlicher Service wird Docker-in-Docker gestartet und für das Bauen der Images verwendet. Das hat den Vorteil, dass der Kubernetes Cluster eine andere Container-Runtime verwenden kann, wie z.B. CRI-O. Zudem sind die Builds damit sehr gut isoliert.

Zur Illustration wird das git-Clone nicht von dem im gitea-Webhook mitgeteilten Repository vorgenommen, sondern eine eigene URL verwendet. Das kann sinnvoll sein, wenn gitea innerhalb des Clusters liegt, und ohne die externe URL angesprochen werden soll. (Dann muss natürlich die Konfiguration des Drone Deployments ebenfalls diese URL verwenden, damit Drone sich bei gitea registrieren kann.)

Beispiel für Drone in Kubernetes
---
#sample pipeline for kubernetes ci/cd
#using Docker-in-Docker with host-persistent storage
#to cache docker images.
kind: pipeline
name: default

platform:
  os: linux
  arch: arm64

services:    #(1)
- name: docker
  image: docker:dind
  privileged: true
  command: [ "--insecure-registry=registry.docker-registry:5000" ]
  volumes:
  - name: docker
    path: /var/lib/docker  #(2)
  ports:
  - 2375


clone:
  disable: true #(3)

steps:
- name: clone
  image: docker:git
  commands:   #(4)
  - git clone http://gitea.gitea.svc.cluster.local:3000/master/docker-ng-cli.git .
  - git checkout $DRONE_COMMIT

- name: build
  image: docker:dind
  environment:
    DOCKER_HOST: tcp://docker:2375  #(5)
  commands:
    - docker version
    - docker build -t registry.docker-registry:5000/ng-cli .  #(6)
    - docker push registry.docker-registry:5000/ng-cli


- name: image-cleanup
  image: docker:dind
  environment:
    DOCKER_HOST: tcp://docker:2375
  commands:
    - docker container prune -f
    - docker volume prune -f
    - docker image prune -f -a --filter "until=24h"

volumes:
- name: docker
  host:
    path: /var/tmp/docker
  1. Docker Daemon als Service - wichtig ist die Angabe des Ports

  2. Host-Verzeichnis zum Cachen von Images

  3. Deaktivierung des automatischen git-Clone

  4. Verwendung eines eigenen git-Clone Kommandos

  5. Verwendung des Docker-Service als Daemon

  6. Regulärer Build eines Docker-Image aus einem Dockerfile im git-Repository

Das Beispiel baut ein Docker-Image aus den Quellen im git Repository und pusht das Image anschließend in eine Docker Registry. Der nächste Schritt könnte nun sein, das Image in einem passenden (Test-)Deployment in Kubernetes zu verwenden.

Nachdem der Build abgeschlossen ist, baut Drone alle dafür benötigten Resourcen wie Pods und Services ab. Zur Isolation verwendet Drone einen temporären Kubernetes Namespace, so dass dieser lediglich gelöscht werden muss und Kubernetes das Aufräumen übernimmt.

Trusted Repository

Falls Services oder andere Container zum Einsatz kommen sollen, die als privileged gestartet werden, muss das Repository als trusted markiert werden. Diese Konfiguration kann ein Drone Administrator in den Repository Settings auswählen.

Bei der Beispielpipeline wird ein Docker-in-Docker Container für den Build der Docker Images im Kubernetes verwendet. Docker-in-Docker setzt vorraus, dass der Container privilegiert gestartet wird, wie in dem Abschnitt des docker Service in der Pipline zu sehen. Daher muss das Repository dann entsprechend konfiguriert werden.

Fazit

Die Kombination aus gitea und Drone stellt eine sehr leichtgewichtige Lösung dar, mit der sich auch komplexe Builds realisieren lassen. Besonders gut gefällt die Integration von Kubernetes als Laufzeitumgebung für Builds in der kommenden 1.0 Version von Drone.

Da das Setup sehr einfach nachvollziehbar ist, eignet sich Drone mit gitea sehr gut, wenn das Thema CI/CD mit Containern erforscht wird oder auch im Kontext von Trainings.




Zu den Themen Kubernetes, Docker und Cloud Architektur bieten wir sowohl Beratung, Entwicklungsunterstützung als auch passende Schulungen an:

Auch für Ihren individuellen Bedarf können wir Workshops und Schulungen anbieten. Sprechen Sie uns gerne an.

Feedback oder Fragen zu einem Artikel - per Twitter @triondevelop oder E-Mail freuen wir uns auf eine Kontaktaufnahme!

Los geht's!

Bitte teilen Sie uns mit, wie wir Sie am besten erreichen können.