Von der ersten Code-Zeile bis zum Betrieb auf Kubernetes – ein Abriss
30.03.2022
Für den Betrieb großer Applikationslandschaften hat sich Kubernetes über Jahre hinweg fest in der Industrie etabliert und ermöglicht Anwendern eine effiziente Nutzung von Ressourcen aus der Cloud. Doch wie sieht das konkret in der Praxis aus? Wie soll man beginnen? Ziel des Artikels ist, dass Sie einen eigenen Container mit einem Golang-Webservice bauen, den entstandenen Code in Git versionieren und diesen in Kubernetes ausrollen und das alles soll rudimentär automatisiert sein.
Sie benötigen:
- Einen GitHub-Account (https://www.github.com)
- Einen AWS-Account mit etwas Spielgeld (https://console.aws.amazon.com)
- Die GitHub CLI (https://cli.github.com/)
- Die AWS CLI (https://aws.amazon.com/cli/)
- Eine IDE, z.B. Visual Studio Code (https://code.visualstudio.com/)
- Linux (als Desktop-Betriebssystem)
Erstellung unseres Webservices
Der Webservice
Als erstes erstellen Sie einen kleinen ″Hello World″-Webservice, welcher nicht nur die Welt grüßen kann, sondern auch beliebige weitere Dinge, die der User in Form eines URL-Path-Parameters in seinem HTTP-Request mitgeben kann. Das kann zum Beispiel ″Dominik″ sein. Es wird dann ″Hello, Dominik!″ als Response ausgegeben. Da- bei nutzen Sie nur die von Go mitgelieferten Standard-Librarys ″http″ und ″fmt″.
package main
import (
″fmt″″net/http″
)
func main() {
http.HandleFunc(″/″, HelloServer) http.ListenAndServe(″:8080″, nil)
}
func HelloServer(w http.ResponseWriter, r *http.Request) {
fmt.Fprintf(w, ″Hello, %s!″, r.URL.Path[1:])
}#
Sourcecode-Verwaltung mit GitHub
Falls Sie noch kein Login auf GitHub besitzen, registrieren Sie sich auf der Webseite github.com und laden sich den Client herunter (https://cli.github.com/). Sie können alle Schritte aus diesem Tutorial auch direkt auf der GitHub-Webseite durchführen – wir empfinden es aber auf einer Linux-Konsole als angenehmer.
Im ersten Schritt richten wir die GitHub CLI mit dem Befehl ″~ gh auth login″ ein:
~ gh auth login
- Folgen Sie nun den Anweisungen
Nach erfolgreicher Anmeldung ist die Git-Hub CLI mit Ihrem Konto verknüpft und kann verwendet werden.
Legen Sie jetzt mit dem Befehl
~ gh re- po create from-code-to-k8s-deployment --private
ein neues Repository mit dem Namen ″from-code-to-k8s-deployment″ an.
Richten Sie nun mit ″~ git init″ Ihr lokales Git-Repository in Ihrem Arbeitsverzeichnis ein.
~ #setzen des systemweiten Default-Branch-Namens auf 'main'
~ git config --global init.defaultBranch main
~ #Initialisierung des git repos
~ git init
~ #hinzufuegen der README.md zum lokalen repo
~ git add README.md
~ #Ausfuehren des resten commits
~ git commit -m "add README.md"
~ # Einbindung des bereits erzeugent remote repos von github.com
~ git remote add origin git@github.com:x-cellent/from-code-to-k8s-deployment.git
~ # Erster push in das main repo
~ git push -u origin main
Jetzt schieben Sie den Go-Code des Webservices in den Ordner ″code″. Er wird committet und in das Remote-Repository gepushet. Das Git-Log sieht nun so aus:
~ git log
commit 62961146f90bbd1a18d8b20710c49a2d6da8837b (HEAD -> main, origin/main)
Author: Dominik Bittl <dominik.bittl@gmail.com>
Date: Tue Feb 8 19:46:06 2022 +0100
add code folder
commit a1abb6515dc89b9b36d9aa4d5d6e12506c3e0797
Author: Dominik Bittl <dominik.bittl@gmail.com>
Date: Tue Feb 8 19:43:28 2022 +0100
add *
commit 9650658b49662a2f5f392f880fb146d1e1cefddb
Author: Dominik Bittl <dominik.bittl@gmail.com>
Date: Tue Feb 8 19:36:02 2022 +0100
add README.md
Die Sourcecode-Verwaltung ist nun eingerichtet, der Code des Webservices ist im Repo erstellt und Sie sind bereit das Thema ″Container″ zu beleuchten und die Containerisierung des Webservices anzugehen.
Bauen des Container-Images inklusive des Webservices
Sie Containerisieren jetzt Ihren Webservice. Ein besonderes Augenmerk liegt dabei darauf, ein möglichst kleines Docker Image zu bauen, daher verwenden Sie in diesem Beispiel als Basis-Image ″distroless″ von Google.
Dazu legen Sie ein File namens ″Dockerfile″ an und kopieren das Dockerfile-Beispiel von der distroless-Dokumentation https://github.com/GoogleContainerTools/distroless und passen es leicht an Ihre Bedürfnisse an.
Erstellung Dockerfile
~ cat Dockerfile
# Start by building the application.
FROM golang:1.17-bullseye as build
WORKDIR /go/src/app
ADD code /go/src/app
RUN go get -d -v ./...
RUN go build -o /go/bin/app
# Now copy it into our base image.
FROM gcr.io/distroless/base-debian11
COPY --from=build /go/bin/app /
CMD ["/app"]
# Start by building the application. FROM golang:1.17-bullseye as build
WORKDIR /go/src/app ADD code /go/src/app
RUN go get -d -v ./...
RUN go build -o /go/bin/app
# Now copy it into our base image. FROM gcr.io/distroless/base-debian11 COPY --from=build /go/bin/app /
CMD [″/app″]
Exkurs: Container
Herausforderungen der Containerisierung
Im Rahmen der Containerisierung werden bestehende Anwendungen modularisiert, in Container verpackt und neue Anwendungen, agil als Cloud-Native-Apps oder in Form entkoppelter Microservices, erstellt. Das vereinfacht die Arbeit für Betriebsteams, da die Container unabhängig von ihrem Inhalt mit einer standardisierten Schnittstelle verarbeitet werden können. Nur stellt sich unweigerlich die Frage, wie Sie die wachsende Menge an Containern nachvollziehbar und mit vertretbarem Aufwand betreiben sollen. Nur mit Docker und einem Automatisierungstool wie "Ansible", stoßen Sie schnell an die Grenzen des Machbaren.
Container-Orchestrierung
Mit Docker wird der Lebenszyklus einzelner Container verwaltet. Orchestrierungs-Werkzeuge (wie z.B. Kubernetes) ermöglichen das Management komplexer Multi-Container-Workloads, die verteilt in einem Cluster aus vielen Maschinen betrieben werden. Container-Orchestratoren sind so etwas wie das "Betriebssystem" eines Rechenzentrums, das für die Abstraktion der einzelnen Server und die einheitliche Bereitstellung von CPU-Rechenkapazität, RAM-Speicher, Plattenspeicher und Netzwerk sorgt.
Durch die Abstraktion der Host-Infrastruktur ermöglichen die Orchestrierungs-Werkzeuge, sich von dem individuellen Management und Rollouts auf einzelne Hosts zu verabschieden und die gesamten Cluster als ein einheitliches Deployment-Ziel zu betrachten. Eine Konsequenz davon ist, dass sich der Betrieb damit grundsätzlich wandelt. Diese Denkweise schlägt sich auch in der Anwendungsentwicklung nieder: Statt zu versuchen, einzelne Service-Instanzen auf möglichst leistungsfähigen Maschinen immer verfügbar zu halten, liegt der Fokus nun darauf, Hochverfügbarkeit und Skalierung durch viele verteilte, idealerweise zustandslose, Service-Instanzen auf abstrakten Compute-Ressourcen zu erreichen.
Bauen des Images
Jetzt führen Sie den Image Build aus: Zuerst müssen Sie aber noch die Go-Modules initialisieren, ansonsten schlägt das Kompilieren des Go-Codes fehl:
~ # Wechseln in das code-Verzeichnis
~ cd code
~ #go modules initialisieren
~ go mod init modules
~ # Zurueck wechseln in den Projekt-Root-Ordner
~ cd ..
~ # Ausfuehren des docker image builds
~ docker build -t webservice .
~ # Anschauen des Ergebnisses mit
~ docker image ls
REPOSITORY TAG IMAGE ID CREATED SIZE
webservice latest 47bf5fd0580d 9 seconds ago 26.4MB
<none> <none> 48254b166a91 16 seconds ago 947MB
golang 1.17-bullseye 80d9a75ccb38 12 days ago 941MB
gcr.io/distroless/base-debian11 latest 24787c1cd2e4 52 years ago 20.2MB
Das neu gebaute Image ist mit etwa 26 MB recht klein und sorgt somit später für eine schnelle Skalierung und geringen Ressourcenbedarf in Ihrem Kubernetes-Cluster.
Um Ihr Webservice-Image lokal zu testen, können Sie den Container schon einmal mit ″~ docker run -p 80:8080 welcome″ starten.
~ # wie exposen den Port des Containers '8080' auf den Host-Port '80'
~ docker run -p 80:8080
Wenn Sie den Webservice im Browser mit
″http://localhost:80″ aufrufen, sehen Sie die Webseite.

Damit haben Sie den ersten Abschnitt gemeistert. Jetzt geht es weiter, indem Sie das Image in Ihre Docker-Registry hochladen.
Upload des Images in die Docker-Registry
Als erstes konfigurieren Sie, falls noch nicht geschehen, die AWS CLI, da wir für unser Beispiel eine private AWS-Registry verwenden möchten. Falls nötig, sehen Sie sich dazu folgenden Dokumentation an: https://docs.aws.amazon.com/cli/latest/userguide/cli-chap-welcome.html
Wichtiger Hinweis: Um das Tutorial nachzubauen, benötigen Sie AWS-Ressourcen, die Geld kosten – Es ist leider nicht möglich das kostenlos zu machen.
Die Erstellung der privaten Registry geht mit ″~ aws ecr create-repository″
~ # ich arbeite aktuell noch mit der aws-cli v1
~ aws ecr create-repository --repository-name webservice --image-scanning-configuration scanOnPush=true
Pushen kann man das von Ihnen erstellte Image dann mit ″~ docker push″
~ # Holen des Anmeldetokens
~ aws ecr get-login-password --region eu-central-1 | docker login --username AWS --password-stdin 1471238641122.dkr.ecr.eu-central-1.amazonaws.com
~ # Bauen des Images (zur vollstaendigkeit nochmals gemacht)
~ docker build -t webservice .
~ # Nun vergeben wir einen Versions-Tag hier 'latest'
~ docker tag webservice:latest 1471238641122.dkr.ecr.eu-central-1.amazonaws.com/webservice:latest
~ # Und jetzt wird das Image hochgeladen in die Registry
~ docker push 1471238641122.dkr.ecr.eu-central-1.amazonaws.com/webservice:latest
Das Image ist nun hochgeladen und kann im nächsten Schritt verwendet werden.
~ docker push 1471238641122.dkr.ecr.eu-central-1.amazonaws.com/webservice:latest
The push refers to repository [1471238641122.dkr.ecr.eu-central-1.amazonaws.com/webservice]
eb642fdcf599: Pushed
0b3d0512394d: Pushed
5b1fa8e3e100: Pushed
latest: digest: sha256:060c8cec4ee49d6dc6ae6c4ca7cb7a8c15990f1ecb00172dd6b06abfcccd0d21 size: 949
Erstellung der ersten CI/CD Pipelines
Kurzer Recap: Sie haben den Webservice programmiert, das Image erstellt, ein Image Repository angelegt und das Image gepusht. Nun wird es Zeit alles zusammen zu führen und zu automatisieren: Dazu erstellen Sie im ersten Schritt eine ″IAM Policy″, um diese mit einem neuen AWS User ′github-action′ zu verknüpfen. Dieser wird dann in Ihren GitHub Actions verwendet.
Ihre IAM Policy liegt unter ′./aws/iam/ecr- policy.json′:
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": [
"ecr:GetAuthorizationToken",
"ecr:BatchCheckLayerAvailability",
"ecr:GetDownloadUrlForLayer",
"ecr:GetRepositoryPolicy",
"ecr:DescribeRepositories",
"ecr:ListImages",
"ecr:DescribeImages",
"ecr:BatchGetImage",
"ecr:GetLifecyclePolicy",
"ecr:GetLifecyclePolicyPreview",
"ecr:ListTagsForResource",
"ecr:DescribeImageScanFindings",
"ecr:InitiateLayerUpload",
"ecr:UploadLayerPart",
"ecr:CompleteLayerUpload",
"ecr:PutImage"
],
"Resource": "webservice"
}
]
}
Angelegt wird diese mit ″~ aws iam create″.
~ aws iam create-policy --policy-name ecr-policy --policy-document file://aws/iam/ecr-policy.json
Das Erstellen des neuen Users erledigen wir mit ″aws iam create user″
~ aws iam create-user --user-name github-actions
{
"User": {
"Path": "/",
"UserName": "github-actions",
"UserId": "AIDARE5NEGHMGXET3WZ33",
"Arn": "arn:aws:iam::1471238641122:user/github-actions",
"CreateDate": "2022-02-09T14:05:10Z"
}
}
und verknüpfen die Policy mit dem User: ″aws iam attach-user-policy″
~ # Die policy-arn kopieren von der Ausgabe zuvor
~ aws iam attach-user-policy --policy-arn arn:aws:iam::1471238641122:policy/ecr-policy --user-name github-actions
Automatisches Bauen des Containers mit Hilfe von GitHub-Actions
Damit die GitHub-Actions-Pipeline Zugriff auf die private AWS Registry bekommt, erstellen wir im Folgenden die User Credentials und hinterlegen diese als CI Secret in GitHub.
Generieren des AWS-Access-Keys für ″github-actions″
~ aws iam create-access-key
{
"AccessKey": {
"UserName": "github-actions",
"AccessKeyId": "AKIXXXXXXXXXXXXXXXXPU",
"Status": "Active",
"SecretAccessKey": "HD9K7FXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXEjfR",
"CreateDate": "2022-02-10T13:26:47Z"
}
}
Außerdem ″AWS Account ID″
~ aws sts get-caller-identity --query "Account"
1471238641122
Erstellen Sie die Secrets im GitHub Repository mit folgendem Befehl ″~ gh secret set″ und kopieren Sie die entsprechenden Werte aus der letzten Ausgabe nach Auf- forderung hinein:
~ gh secret set AccessKeyId -r x-cellent/from-code-to-k8s-deployment
? Paste your secret ********************
✓ Set secret AccessKeyId for x-cellent/from-code-to-k8s-deployment
~ gh secret set SecretAccessKey -r x-cellent/from-code-to-k8s-deployment
? Paste your secret ****************************************
✓ Set secret SecretAccessKey for x-cellent/from-code-to-k8s-deployment
~ gh secret set AWSACCOUNTID -r x-cellent/from-code-to-k8s-deployment
? Paste your secret ************
✓ Set secret AWSACCOUNTID for x-cellent/from-code-to-k8s-deployment
Nun können Sie die GitHub Action erstellen. Was soll diese können?
- Automatisch starten, wenn sich Änderungen auf dem main-Branch ergeben
- Das Image neu bauen
Das neue Image in die AWS Registry hochladen
name: create-and-push-image on: [push] jobs: build-and-push: runs-on: ubuntu-latest steps: - uses: actions/checkout@v2 - uses: kciter/aws-ecr-action@v3 with: access_key_id: ${{ secrets.ACCESSKEYID }} secret_access_key: ${{ secrets.SECRETACCESSKEY }} account_id: ${{ secrets.AWSACCOUNTID }} repo: webservice region: eu-central-1 tags: latest,${{ github.sha }}
Sie nutzen dabei eine GitHub Action aus dem Marketplace, die Sie hier finden: https://github.com/kciter/aws-ecr-action
Gespeichert wird das yaml-File in Ihrem Entwicklungsordner unter ′.github/work- flows/create-and-push-image.yml′.
Das Ganze wird dann, wie gewohnt mit 'git commit' und 'git push', in Ihr Github Repository gepusht und schon läuft der Workflow los, baut und published das Image.
Exkurs: Kubernetes
Containerisierung erfordert, wie oben bereits erläutert, auch Automatisierung im Betrieb. Kubernetes ist der de facto Standard in diesem Bereich - bringt aber auch eigene Komplexität, sowie eine steile Lernkurve mit sich und erfordert Veränderungen in Anwendungsarchitektur und Betrieb.
Kubernetes
Kubernetes ist eine Open Source Plattform, mit der containerisierte Anwendungen bereitgestellt, verwaltet, überwacht und automatisch skaliert werden können. Es abstrahiert die zugrunde liegende Infrastruktur und stellt eine API für deklaratives Management von Container-Workloads bereit - und vereinfacht dadurch die Entwicklung, Bereitstellung, und Management containerbasierter Anwendungen.
Architektur
Ein Kubernetes-Cluster besteht aus mehreren physischen oder virtuellen Maschinen (Nodes), die in zwei Typen unterteilt werden: Master-Nodes, auf welchen sich die Kubernetes-Steuerungsebene befindet und Worker-Nodes, auf denen "Pods" ausgeführt werden. "Pods" sind die kleinste auf den Worker-Nodes einsetzbare Einheit und enthalten einen oder auch mehrere Container.

Einrichtung eines managed Kubernetes-Clusters auf AWS
Mit dem installierten ″aws eksctl″ erzeugen Sie jetzt einen managed-Kubernetes-Cluster auf AWS. Diesen nutzen Sie dann für Ihren Webservice.
Setzen von Variablen:
export CLUSTERNAME="mein-cluster"
ACCOUNT_ID=$(aws sts get-caller-identity --query "Account" --output text)
~ # Der Befehl erzeugt ein Cluster mit dem Namen 'mein-cluster' in der Region Frankfurt im Mode 'fargate', so dass wir keine Nodes managen muessen.
~ eksctl create cluster --name $CLUSTERNAME --region eu-central-1 --fargate
Testen des Zugriffs auf den Cluster:
~ kubectl get nodes
NAME STATUS ROLES AGE VERSION
fargate-ip-192-168-130-109.eu-central-1.compute.internal Ready <none> 1m v1.21.2-eks-06eac09
fargate-ip-192-168-161-58.eu-central-1.compute.internal Ready <none> 1m v1.21.2-eks-06eac09
Jetzt müssen Sie noch einige AWS-spezifische Anpassungen am Cluster vornehmen, damit die Loadbalancer-Integration und somit das Publishen des Webservices sauber funktioniert:
~ # Damit der Cluster AWS Identity and Access Management (IAM) für Servicekonten verwenden kann
~ eksctl utils associate-iam-oidc-provider --cluster $CLUSTERNAME --approve
~ # Einrichten der IAM Policy fuer den AWS Loadbalancer Controller
~ curl -o iam_policy.json https://raw.githubusercontent.com/kubernetes-sigs/aws-load-balancer-controller/v2.2.0/docs/install/iam_policy.json
~ aws iam create-policy --policy-name AWSLoadBalancerControllerIAMPolicy --policy-document file://iam_policy.json
~ # Um ein Servicekonto mit dem Namen aws-load-balancer-controller im kube-system-Namensraum für den AWS Load Balancer Controller zu erstellen (arn mit der richtigen account-id anpassen nicht vergessen)
~ eksctl create iamserviceaccount \
--cluster=$CLUSTERNAME \
--namespace=kube-system \
--name=aws-load-balancer-controller \
--attach-policy-arn=arn:aws:iam::$ACCOUNT_ID:policy/AWSLoadBalancerControllerIAMPolicy \
--override-existing-serviceaccounts \
--approve
Als nächstes fügen Sie das entsprechende Installations-Repo hinzu:
~ # Damit der Cluster AWS Identity and Access Management (IAM) für Servicekonten verwenden kann
~ eksctl utils associate-iam-oidc-provider --cluster $CLUSTERNAME --approve
~ # Einrichten der IAM Policy fuer den AWS Loadbalancer Controller
~ curl -o iam_policy.json https://raw.githubusercontent.com/kubernetes-sigs/aws-load-balancer-controller/v2.2.0/docs/install/iam_policy.json
~ aws iam create-policy --policy-name AWSLoadBalancerControllerIAMPolicy --policy-document file://iam_policy.json
~ # Um ein Servicekonto mit dem Namen aws-load-balancer-controller im kube-system-Namensraum für den AWS Load Balancer Controller zu erstellen (arn mit der richtigen account-id anpassen nicht vergessen)
~ eksctl create iamserviceaccount \
--cluster=$CLUSTERNAME \
--namespace=kube-system \
--name=aws-load-balancer-controller \
--attach-policy-arn=arn:aws:iam::$ACCOUNT_ID:policy/AWSLoadBalancerControllerIAMPolicy \
--override-existing-serviceaccounts \
--approve
und installieren den ″AWS Load Balancer Controller″:
~ # Helm Repo hinzufuegen
~ helm repo add eks https://aws.github.io/eks-charts
~ # Installation der TargetGroupBIondings
~ kubectl apply -k "github.com/aws/eks-charts/stable/aws-load-balancer-controller//crds?ref=master"
~ # Ermitteln der VPC-ID der vorher erstellten EKS-Umgebung
~ VPC=$(aws cloudformation describe-stacks --query 'Stacks[?StackName==`eksctl-mein-cluster-cluster`][].Outputs[?OutputKey==`VPC`].OutputValue' --output text)
~ # Installieren des Controllers
~ helm install aws-load-balancer-controller eks/aws-load-balancer-controller \
--set clusterName=$CLUSTERNAME \
--set serviceAccount.create=false \
--set region=eu-central-1 \
--set vpcId=$VPC \
--set serviceAccount.name=aws-load-balancer-controller \
-n kube-system
NAME: aws-load-balancer-controller
LAST DEPLOYED: Thu Feb 17 00:42:13 2022
NAMESPACE: kube-system
STATUS: deployed
REVISION: 1
TEST SUITE: None
NOTES:
AWS Load Balancer controller installed!
Erstellung eines Helm-Charts für das Deployment des Webservices
Im Ordner ″deploy″ bauen Sie mit Hilfe von kustomize (https://kustomize.io/) einen Helm-Chart:
# Mit "kustomize build" kann man das komplette Helm-Chart ausgeben
~ cd deploy
~ kustomize build
apiVersion: v1
kind: Service
metadata:
annotations:
service.beta.kubernetes.io/aws-load-balancer-nlb-target-type: ip
service.beta.kubernetes.io/aws-load-balancer-scheme: internet-facing
service.beta.kubernetes.io/aws-load-balancer-type: external
labels:
app: webservice
name: webservice
spec:
ports:
- port: 80
protocol: TCP
targetPort: 8080
selector:
app: webservice
type: LoadBalancer
---
apiVersion: apps/v1
kind: Deployment
metadata:
labels:
app: webservice
name: webservice-deployment
spec:
replicas: 3
selector:
matchLabels:
app: webservice
template:
metadata:
labels:
app: webservice
spec:
containers:
- image: 1471238641122.dkr.ecr.eu-central-1.amazonaws.com/webservice:latest
name: webservice-container
ports:
- containerPort: 8080
resources:
limits:
cpu: "0.5"
memory: 200Mi
requests:
cpu: 100m
memory: 100Mi
Dieses kann nun mit folgenden Befehlen auf dem Cluster installiert werden ″kubectl apply -k″:
kubectl apply -k .
Der Cluster zieht sich nun Ihr Docker-Image aus Ihrer Docker-Registry, legt den Loadbalancer an exponiert den Webservice im Internet.
kubectl get services
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
kubernetes ClusterIP 10.100.0.1 <none> 443/TCP 17m
webservice LoadBalancer 10.100.253.113 k8s-default-webservi-1a6d153cab-e576e43b4b4351aa.elb.eu-central-1.amazonaws.com 80:30000/TCP 7s
Nach einer kurzen Wartezeit kann der Webservice über den automatisch generierten DNS-Namen im Browser aufgerufen werden.

Wichtig: Es entstehen Kosten bei AWS, wenn Sie dieses Beispiel nachbauen. Daher sollten Sie darauf achten, die angelegten Ressourcen wieder zu löschen. Dies können Sie mit den folgenden Befehlen tun:
~ eksctl de- lete cluster –name=$CLUSTERNAME
Und danach prüfen Sie bitte nochmals, ob wirklich alles gelöscht wurde.
Sicher ist sicher!