.Net Core alkalmazás futtatása Azure Kubernetes Service-ben

2019. 09. 26.

A Docker, konténerizáció és Kubernetes szavak az utóbbi években a szoftverfejlesztés alapfogalmai közé ivódtak. Ma már szinte minden magára valamit is adó .net fejlesztőnek érdemes rendelkezni olyan szintű dev-ops ismeretekkel, amelyek lehetővé teszik számára konténerizált alkalmazások futtatását mind local, mind távoli környezeteken.

Az eNET fejlesztő csapata már évek óta foglalkozik konténerizált alkalmazások fejlesztésével és üzemeltetésével, Microsoft Gold Partnerségünk lévén pedig egyértelmű választás volt, hogy a modern felhő szolgáltatások közül az Azure-t lesz az, ahol távoli fejlesztői és éles környezeteinket is felépítjük és üzemeltetjük.

Ezen cikk célja, hogy egy egyszerűbb példa alkalmazáson keresztül bemutassa, hogy miképp helyezhetünk ki konténerizált alkalmazásokat Azure Kubernetes Service-ben (AKS) futó cluster-be komplett Azure DevOps CI/CD és Helm segítségével. Nem célja a technológia mély ismertetése így a miértekre és hogyanokra nem is térünk ki részletesen, inkább egyfajta kiindulási pontként hivatott szolgálni.

Miért épp Docker?

konténer a szoftver és annak függőségeinek olyan egysége, amely lehetővé teszi az alkalmazás gyors futtatását több környezetben is. A Docker container image egy lightweight, különálló szoftvercsomag, amely mindent tartalmaz, ami az alkalmazásunk futtatásához szükséges (kód, függőségek, rendszer eszközök és beállítások). Lényegében ugyan az az alkalmazás futtatható a fejlesztő saját gépén, mint az éles környezetben, így számtalan olyan hibalehetőség megelőzhető, amikre csak éles környezetben derülne fény. A jól felépített dockerizált környezetben nem hangozhat el a „De az én gépemen működik!” mondat.

Kubernetes és Helm

Kubernetes (K8s) egy open-source rendszer konténerizált alkalmazások kitelepítésére, skálázására és menedzselésére. Logikai egységekbe csoportosítja a konténereket az egyszerűbb kezelés érdekében.

Helm segítségével a K8s-s alkalmazásaink menedzselését tehetjük hatékonyabbá. A Helm Chart-okban definiálhatjuk a K8s installációnk sablonjait és szerkezetét, a Helm Value fájlokban pedig ezek értékeket kapnak.

Nézzük hát meg, miképp is helyezhetünk ki fejlesztői gépről Azure környezetbe egy konténerizált alkalmazást! Itt szükséges megjegyezni, hogy a demo alatt végig Windows-on dolgozunk és Microsoft-os eszközöket használunk, továbbá a folyamat lekövetéséhez szükséges egy Azure előfizetés, valamint Azure DevOps fiók.

Szükséges előfeltételek:

  • .Net Core SDK (A demo alatt a 3.0-s verziót használjuk)
  • Docker lokálisan telepítve (Docker for Windows)
  • Kubernetes (ez a Docker beállításokból bekapcsolható)
  • Helm
  • Azure CLI
  • Azure Dev-Ops repository
  • Azure előfizetés

1. Repository létrehozása

Első lépésként szükségünk van egy tárhelyre az alkalmazás kódjának, illetve ennek segítségével fogjuk tudni a szükséges Continous Integration/Continous Delivery/Deployment lépéseket kezelni és azokat beállítani. Új- vagy meglévő DevOps fiókunkban hozzunk létre egy projektet, adjunk neki nevet, lépjük bele majd a Repos menüpontra kattintva lehetőségünk van egy üres repository inicializálására. Klónozzuk le a repository-t saját gépünkre. A repository és projekt létrehozása bármilyen módon történhet, a demo kedvéért ezt az egyszerű megoldást választottuk.

2. .Net alkalmazás létrehozása és kód feltöltése a repository-ba:

Következő lépés a konkrét .Net Web Application elkészítése. Ezt most a demo kedvéért Visual Studio segítségével tesszük, de lehetőség van ezt CLI használatával is megtenni (Akár Visual Studio Code-al vagy standard parancssorral). A VS segítségével el is tudjuk indítani az alkalmazásunkat. CLI-t megnyitva, ha beírjuk a ’dokcer ps’ parancsot láthatjuk az éppen futó konténereket, többek közt azt amit most kreáltunk. Most már push-olhatjuk változtatásainkat a repository-ba.

3. Container Registry (CR) létrehozás és Continuous Integration(CI):

A Container Registry a docker image-k tárolására szolgál, az AKS innen fogja majd „pull-oln”i az imaget. Az Azure az ott használt erőforrásainkat ún. Resource Group-okba csoportosítja. Hozzunk létre egy Resource Group-ot, majd azon belül egy Container registry-t.

Következő lépésként el kell érnünk, hogy a létrehozott CR-be bekerüljön a docker image. Ezt az Azure DevOps-ban beállított Build Pipeline-nal fogjuk elérni. A klasszikus „kattingatós” felületet fogjuk választani a látványosabb szemléltetés végett.  Válasszuk ki a repository-nkat és annak master branch-ét a build forrásaként. Ezután a felajánlott Agent job listából válasszuk ki a Docker-t és a hozzá adott 2 Build lépésben állítsuk be cél CR-nek a korábban létrehozott Registry-t. Mentsünk és indítsuk is el a Build-et.

A sikeres lefutás után a CR-ben megjelenik az image.

4. Helm

Hozzunk létre egy ’helm’ könyvtárat az applikáción belül és ott futtassuk le a ’helm create aksdemo’ parancsot, létrehozva ezzel a default Helm chat-okat és values fájlt.

A values fájlt egészítsük ki a következő képpen, hogy az alkalmazás igényeink szerint induljon el:

---
apiVersion: apps/v1
kind: Deployment
metadata:
  name: {{ .Values.nameOverride }}-deployment
  labels:
    app: {{ .Values.nameOverride }}-deployment
spec:
  replicas: {{ .Values.replicaCount }}
  selector:
    matchLabels:
     app: {{ .Values.nameOverride }}
  template:
    metadata:
      name: {{ .Values.nameOverride }}
      labels:
        app: {{ .Values.nameOverride }}
        purpose: {{ .Values.nameOverride }}
    spec:
{{- if .Values.affinity }}
      affinity:
{{ .Values.affinity | toYaml | trimSuffix "\n" | indent 8 }}
{{- end -}}   
{{- if .Values.nodeSelector }}
      nodeSelector:
{{ .Values.nodeSelector | toYaml | trimSuffix "\n" | indent 8 }}
{{- end -}}
{{- if .Values.securityContext }}
      securityContext:
{{ .Values.securityContext | toYaml | trimSuffix "\n" | indent 8 }}      
{{- end }}
{{- if or .Values.volumeMounts .Values.certs }}
      volumes:
{{- if .Values.volumeMounts }}
      - name: {{ .Values.nameOverride }}-storage
        persistentVolumeClaim:
         claimName: {{ .Values.nameOverride }}-pvc
{{- end}}
{{- if .Values.certs }}
      - name: ssl-config
        configMap:
          name:  {{ .Values.nameOverride }}-ssl-config
          defaultMode: 0777
{{- end }}
{{- end }}
      containers:
      - name: {{ .Values.nameOverride }}
        image: {{ .Values.image.repository }}:{{ .Values.image.tag }}
        imagePullPolicy: {{ .Values.image.pullPolicy }}
{{- if .Values.env }}
        envFrom:
        - configMapRef:
            name: {{ .Values.nameOverride }}
{{- end}}
        ports:
        - containerPort: {{ .Values.container.port }}
          name: http         
{{- if or .Values.volumeMounts .Values.certs }}
        volumeMounts:
  {{- if .Values.volumeMounts }}
        - mountPath: {{ .Values.volumeMounts.path }}
          name: {{ .Values.nameOverride }}-storage
  {{- end }}
  {{- if .Values.certs }}
        - name: ssl-config
          mountPath: {{ .Values.certs.path }}
  {{- end }}
{{- end }}
{{- if .Values.resources }}
        resources:
{{ .Values.resources | toYaml | trimSuffix "\n" | indent 10 }}
{{- end }}
{{- if .Values.imageCredentials }}
      imagePullSecrets:
      - name:  {{ .Values.nameOverride }}-gitlab-secret
 {{- end }}
---
{{- if .Values.service }}
kind: Service
apiVersion: v1
metadata:
  name: {{ .Values.nameOverride }}-service
spec:
  type: {{ .Values.service.type }}
  selector:
    app: {{ .Values.nameOverride }}
  ports:
  - protocol: {{ .Values.service.protocol }}
    port: {{ .Values.container.port }}
    targetPort: {{ .Values.container.port }}
    {{- if eq .Values.service.type "NodePort" -}}
    nodePort: {{ .Values.service.nodePort }}
    {{- end }}
{{- end }}
{{- if .Values.ingress }}
apiVersion: extensions/v1beta1
kind: Ingress
metadata:
  name: {{ .Values.nameOverride }}-ingress
{{- if .Values.ingress.annotations }}
  annotations:
{{ .Values.ingress.annotations | toYaml | trimSuffix "\n" | indent 4 }}
{{- end }}
spec:
{{- if .Values.ingress.tls }}
  tls:
{{ .Values.ingress.tls | toYaml | trimSuffix "\n" | indent 4 }}
{{- end }}
  rules:
  - host:  {{ .Values.ingress.host }}
    http:
      paths:
      - path: {{ .Values.ingress.path }}
        backend:
          serviceName: {{ .Values.ingress.serviceName }}
          servicePort: {{ .Values.ingress.servicePort }}
{{- end }}
replicaCount: 1

image:
  repository: aksdemorc.azurecr.io/aks-demo
  tag: latest
  pullPolicy: IfNotPresent

nameOverride: "aksdemo"
fullnameOverride: "aksdemo"

ingress:
  annotations:
    kubernetes.io/ingress.class: nginx
  tls:
  - hosts:
    - aks-demo.westeurope.cloudapp.azure.com 
    secretName: aks-demo-tls
  host: aks-demo.westeurope.cloudapp.azure.com
  path: /
  serviceName: aksdemo-service
  servicePort: 80
  tls: []

service:
  type: ClusterIP
  protocol: TCP

container:
  port: 80

resources: {}
nodeSelector: {}
tolerations: []
affinity: {}

Hajtsuk végre a Build pipeline módosításait, hogy a létrejövő Build Artifact tartalmazza a deployment-hez szükséges Helm fájlokat, azaz adjunk hozzá egy ’Copy files’ és egy ’Publish build artifact’ lépést.

5. AKS

Az Azure portálon létre kell hozni az AKS resource-t. A korábban is használt Resource Group-hoz adjunk hozzá az új Kubernetes clustert. Itt választhatunk nevet és régiót, valamint az erőforrásokat és egyéb beállításokat is átszabhatunk. A demóra elegendők a legkisebb virtuális gépek a node-ok számára, illetve kettőnél több pod is fölösleges. Éles környezeteken alkalmazástól függően ezek változhatnak, de ott célszerű a 3 vagy több node.

A Monitoring tab alatt az Application Insights-t kikapcsolhatjuk, illetve ha szeretnénk egyedi service principal-t is megadhatunk a clusternek. Ne lepődjünk meg, az Azure automatikusan létre fog hozni egy külön resource group-t a cluster erőforrásai számára (vm-ek, storage-ok, load balancer). 

Sikeres deploy után a cluster el is érhető. Ha feltelepítettük az Azure CLI-t akkor a standard parancssort megnyitva a következő parancsot kiadhatjuk, saját előfizetés azonosítóval:

az aks get-credentials –resource-group aks-demo-rg –name aks-demo-cluster –subscription {subscription  id}

Ezzel a current-context-be bekerül a távoli cluster, így a helyileg kiadott parancsok a távoli cluster-re fognak vonatkozni. Ez után 2 clusterrolebinging beállítást kell megtennünk, hogy a kubernetes dashboard-ja elérhető legyen, valamint a helm-et könnyen telepíteni tudja a CD.

kubectl create clusterrolebinding kubernetes-dashboard -n kube-system –clusterrole=cluster-admin –serviceaccount=kube-system:kubernetes-dashboard

kubectl create clusterrolebinding default -n kube-system –clusterrole=cluster-admin –serviceaccount=kube-system:defaultaz aks browse –resource-group aks-demo-rg –name aks-demo-cluster –subscription {subscription  id}

A fenti parancsot kiadva böngészőben megjelenik a kubernetes dashboardKövetkező lépésként lehetővé kell tennünk a cluster számára, hogy a CR-ből image-k húzzon le. Ezt legegyszerűbben a portál felületen tehetjük meg. A CR Access control (IAM) menüpontjában adjunk AcrPull jogosultságot (role) a demo cluster service principal-jának.

6. Continuous Delivery/Deployment

Ahhoz, hogy a repository-ban lévő kódunk élő alkalmazássá lépjen elő a build-elt image-ket ki kell a clusterra helyeznünk. Ebben lesz segítségünkre a DevOps-ban configurált Release pipeline.

Hozzunk létre egy újat, és a template-ek közül válasszuk a Deploy an application to a Kubernetes cluster by using its Helm chart opciót. Válasszuk ki a korábban beállított build pipeline-t artifactként és kapcsoljuk is be continuous deployment triggert. Adjuk meg a szükséges adatokat, mint subscription, resource group és cluster illetve a 3. lépésnél a value fájl elérési útvonala.

Extra lépésként szükségünk lesz egy Ingress deployment step-re, hogy a cluster-ünk külsőleg is elérhető legyen. Az Ingress létre hozza a szükséges load balancer-t és a külső IP címet is beállítja. Itt a Helm által adott gyári charot használjuk, ami hasonlóan a docker image-khez, egy stable-ként elnevezett publikus repository-ból elérhető (stable/nginx-ingress).

Sikeres lefutást követően az alkalmazásunk kihelyezésre kerül a clusteren. A dashboard-t elérve láthatjuk a létrehozott pod-okat. Az Azure portálon a cluster erőforrásait tartalmazó resource group-ban megjelent egy Public IP address típusú erőforrás. Ezt kiválasztva annak Configuration menüpontjában adhatjuk meg az a DNS name label-t, amelyet a value fájlban megadtunk (aks-demo).

Az alkalmazásunk immár a megadott dns címen elérhető.

eNET Internetkutató és Tanácsadó Kft.