Kubernetes continuous Integration (CI)
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.
---
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.
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.
--- 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.
---
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.
---
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.
---
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.
---
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.
---
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.)
---
#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
-
Docker Daemon als Service - wichtig ist die Angabe des Ports
-
Host-Verzeichnis zum Cachen von Images
-
Deaktivierung des automatischen git-Clone
-
Verwendung eines eigenen git-Clone Kommandos
-
Verwendung des Docker-Service als Daemon
-
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.