Mikrotjenester en arkitekturstil som strukturerer en applikasjon som en samling av mindre uavhengige tjenester. Dette skiller seg fra den klassiske monolitten som opererer som en selvstendig applikasjon adskilt fra andre systemer.
For smidige team er muligheten til å ha raske leveranser meget verdifull. Hvis man da også kan eliminere nedetid i systemet er det enda bedre! Mikrotjenester har den medfødte egenskapen at hver del av systemet er uavhengig. Dette gjør at de kan utvikles side om side med annen kode og ha uavhengige leveranser av hver del av systemet med granulær kontroll. Ulempen er at det kan være vanskelig å ha god struktur på distribusjon og versjonering av tjenester når størrelsen på clusteret øker.
I dette innlegget skal vi se på hvordan vi bruker Azure Container Registry og Helm for å distribusjon og versjonering av Open Container Initiative (OCI) images og Helm charts for vedlikehold av et Kubernetes cluster i Azure Kubernetes Service.
Docker, containere og image
Veien til et fungerende cluster begynner som regel med å bruke Docker til å bygge et image og en container. Docker er et verktøy som lar deg pakke og kjøre kode i isolerte miljøer kalt containere. Imaget er den laveste byggeklossen i clusteret som blir kjørt av en container og inneholder alt av kode, avhengigheter og binærfiler etc som trengs for å kjøre en applikasjon.
Imaget er også avhengig av et OS for å kjøre og fungerer som en blåkopi av hva som vil være i en container når den kjører. For å lage et image så skriver vi en Dockerfile. Dette er en instruksjon for hvordan Docker skal lage imaget og vil inneholde stack-spesifikke kommandoer for å bygge og pakke en kodebase. Se eksempel under for Dockerfil for et image tilknyttet et dotnet 6 API.
FROM mcr.microsoft.com/dotnet/aspnet:6.0 AS base
WORKDIR /app
EXPOSE 80
FROM mcr.microsoft.com/dotnet/sdk:6.0 AS build
WORKDIR /src
COPY . /src
RUN dotnet restore "*.csproj"
RUN dotnet build "*.csproj" -c Release -o /app/build
FROM build AS publish
RUN dotnet publish "*.csproj" -c Release -o /app/publish
FROM base AS final
WORKDIR /app
COPY --from=publish /app/publish .
ENTRYPOINT ["dotnet", "*.dll"]
Azure Container Registry
Når man har laget et image, må dette lagres et sted så vi kan bruke det senere. Her kan man bruke Dockers egne Hub for images, eller så kan man bruke Azure Container Registry. ACR er en tjeneste som lar deg bygge, lagre og håndtere artefakter i private registre for distribusjon av alle typer containere.
For å få et image inn i ACR trenger vi også en pipeline. Her bruker vi Azure Devops med YAML pipelines ut til den infrastrukturen i Azure vi trenger. Nedenfor er et eksempel av YAML-koden som trengs for å kjøre en Docker task. Denne tasken gjør et bygg og laster opp det ferdige imaget til ACR. Her defineres “repository” som registeret som skal holde det spesifikke imaget og dets versjoner i ACR. Versjon på images spesifiserer som Tags. Hvis man ikke spesifiserer en versjon vil imaget bli tagget med Latest. Når pipelinen er ferdig, vil vi ha et image som kan benyttes i våre mikrotjenester.
task: Docker@2
inputs:
containerRegistry: 'NAVN PÅ ACR RESSURS'
repository: 'NAVN PÅ REPO TIL IMAGET'
command: 'buildAndPush'
Dockerfile: 'src/Dockerfile'
tags: 'v1.0'
Kubernetes
Når vi har et image er det på tide å bygge selve mikrotjenestene som skal brukes i systemet. Grovt sett er kubernetes det inn i noder, podder og containere. Arbeid utføres ved at trafikk til clusteret blir dirigert til en node. Nodene er en arbeidsmaskin i clusteret og kan være f.eks en fysisk eller en virtuell maskin. De er også fordelt inn i master og arbeidsnoder der masternoden overvåker clusteret og sender trafikk til arbeidsnodene. Arbeidsnodene inneholder podder som tar imot trafikken og utfører logikken. En pod er den laveste distribuerbare komponenten i et cluster og inneholder en eller flere containere som igjen kjører imagene (Se figur under for struktur mellom kubernetes ressurser).
Deployments
For å få en pod inn i kubernetes trenger vi en deployment. En deployment forteller kubernetes hvordan en pod skal lages eller modifiseres. Deploymenten spesifiseres gjennom en YAML-fil (ofte kalt deployment-fil) som inneholder all informasjon en pod trenger. Dette innebærer f.eks hva containeren og deploymenten skal hete, hvilket image containeren skal kjøre og hvordan skal man kommunisere. Legg også merke til feltene replicas og image under containers-feltet. Replicas spesifiserer hvor mange instanser av en pod som skal lages, og image som forteller hva slags og hvilken versjon av et image vi ønsker å bruke i containeren vår.
apiVersion: apps/v1
kind: Deployment
metadata:
name: NAVN PÅ DEPLOYMENY
namespace: NAMESPACE I KUBERNETES
spec:
replicas: 1
selector:
matchLabels:
app: NAVN PÅ POD SOM MATCHES
template:
metadata:
labels:
app: NAVN PÅ POD # The name of our pod
spec:
containers:
- name: NAVN PÅ CONTAINER
Image: STI TIL CONTAINER : VERSION
ports:
- containerPort: 80
imagePullPolicy: Always
resources:
limits:
memory: "128Mi"
cpu: "50m"
Helm
Når man har en fullstendig deploymentfil kan man instanser ressursene i clusteret. Dette kan man enten gjøre ved hjelp av Kubernetes’ egne cli kalt “kubectl”, men siden vi skal bygge en CI/CD pipeline for versjonering og kontroll skal vi bruke Helm. Helm er et verktøy for versjonering og håndtering av kubernetes applikasjoner. Ved hjelp av Helm kan man pakketere en kubernetesressurs til et “Helm Chart”. Et chart er en blåkopi av hva som skal distribueres og kan brukes for å redusere mengden kode som må skrives for å få komponenter til et cluster.
I helm så er strukturen delt opp følgende filer:
- Chart.yaml: Dette er informasjonen om chartet som skal lages. Hva det heter, beskrivelse og versjonsnummer og vil se slik ut:
apiVersion: v2
name: NAVN PÅ TJENESTE
description: A Helm chart for Contriva CRM customer Services.
type: application
version: 0.0.1
appVersion: "0.0.1"
- Values.yaml: Dette er alle miljøvariablene som trengs i deploymentfilen som tilhører chartet:
image:
tag: "latest"
deployment:
resources:
limits:
memory: "128Mi
cpu: "100m"
- Templates: Dette er en mappe som inneholder selve deploymentfilen som skal generere ressurser i Kubernetes, og en mappe kalt components. Components inneholder yaml filer for andre typer støttekomponenter som også skal lages sammen med containeren. Dette kan være køer, identiteter og andre ikke-kjørbare ressurer. Deploymentfilen kan da se helt lik ut som vist tidligere, eller så kan vi legge til template variabler som hentes ut fra values.yaml filen. For å sette inn verdier i en variabel fra values-filen over kan vi skrive følgende der man bruker $-for å spesifisere en variabel og for å spesifisere kode. I eksempelet under henter vi ut cpu-variablen fra values-filen.
cpu:
Skal man distribuere flere ressurser samtidig, kan man benytte seg av subcharts. For å bruke subcharts lager vi en global Chart.yaml og values.yaml som inneholder oversikt over avhengigheter til ressursene man vil distribuere og globale verdier som er felles for alle ressurser. Strukturen vil da se slik ut:
- Chart.yaml # Hovedchart for releasen
- Values.yaml # Globale verdier
- Charts
- Ressurs_1
- Chart.yaml # Hovedchart for ressursen
- Values.yaml # Verdier for ressursen
- Templates
- Deployments # Mappe for delkomponenter
- Component.yaml
- Ressurs_2
- ......
Avhengighetene i strukturen kan defineres i det globale chart.yaml med følgende kodeblokk (Se yaml-kode under), der repository spesifiserer hvor vi skal finne mappen der chartet til ressursen ligger.
dependencies:
- name: Pod1
repository: "file://charts/Ressurs_1"
version: 0.0.1
tags:
- "pod nr 1"
- name: Pod2
repository: "file://charts/Ressurs_2"
version: 0.0.2
tags
- "pod nr 2"
Helm + ACR = <3
Når man har Helm på plass, så kan man begynne å designe et CI/CD løp for å vedlikeholde et cluster. Essensen i et slikt opplegg ligger i å bruke ACR for å lagre docker images og helm charts. Deretter bruker vi Helm til å hente ressurser fra ACR og levere dem ut til et cluster. Dette gjør vi ved å ha en bygglinjen som bygger Helm charts og Docker imager med rett versjon og putter disse i ACR.
Bygglinjen
For å inkludere automatisk inkrementell versjonering av helm charts og image, trenger vi først å generere et versjonsnummer og sette dette til chartet og imaget. Dette kan vi gjøre ved å legge inn følgende formel( se formel under) som en variabel i byggelinjene. Her vil majorVersion og minorVersion også være variabler i devops bygglinjen som brukerne kan øke dersom man har leveranse som tilsvarer større endringer enn et byggnr.
$buildVersion = $[counter(format('{0}.{1}', variables['majorVersion'], variables['minorVersion']), 0)]
Dermed har vi et byggnr, og det kan vi bruke direkte i Docker-oppgavene i bygglinjen for å tilføye rett versjon til imaget gjennom tags feltet på kommandoen. Helm chartet krever litt mer innsats da vi må endre versjon og appnr som står i selve chart.yaml filen. For å gjøre dette kan man blant annet bruke powershell og den innebygde modulen powershell-yaml, for å bytte ut verdiene med det nye genererte versjonsnummeret.
Da er det klart for å levere helm-chartet til ACR. Dette kan gjøres med Helm Cli i et inline script i bygglinjen, se kode under. Her benytter vi Devops sine ServiceConnections for få en tilkobling til ressursene i Azure. Vi logger så inn til vårt ACR og laster opp det pakkede Helm Chartet med ArtifactStagingDirectory som mellomlagring for artefaktene. Når dette skjer så vil helm vite hvilken versjon av chartet vi bruker siden vi har byttet ut appversjon og version i chart.yaml-filen.
- task: AzureCLI@2
displayName: Push chart to ACR
inputs:
azureSubscription: "$(serviceConnection)"
scriptType: bash
scriptLocation: inlineScript
inlineScript: |
az acr login --name AzureContainerRegistry
helm package PathToCharts - - $(Build.ArtifactStagingDirectory)
helm push
'$(Build.ArtifactStagingDirectory)/$(service)-$(chartVersion).tgz' "oci://URL TIL DIN ACR/charts/"
Nå vil både Docker images og Helm Charts for kubernetesressurene våre lagret i ACR med rett versjon. Det fine med ACR er at vi får lagret alle versjoner av ressursene våre så vi også kan hente ut gamle versjoner. Med dette på plass så vil vi ha inkrementell versjonskontroll på både images og på charts, så nå må vi begynne å bygge selve distribusjonslinjen ut til clusteret vårt!
Leveranselinjen
For å oppsummere så har vi laget bygglinjer for en ressurs som både genererer et Docker Image og et Helm Chart med inkrementelle versjonsnr og som laster disse opp i ACR. Nå trenger vi å lage en distribusjonslinje i Devops som kan levere ressursene fra ACR til et kubernetes cluster.
Det første vi begynner med er å lage et chart som representerer alt vi ønsker å levere av ressurser med rett versjon og en global values-fil for leveransen. Vi benytter oss da av dependencies blokken i chart-filen, og siden vi henter ressurser fra ACR så vil repository blokken peke på urlen som ACR bruker for å lagre OCI ressurser og ikke en lokal fil. Det vil se følgende ut:
apiVersion: v2
name: helm-services
description: A Helm chart for kubernetes services
type: application
version: 0.1.1
appVersion: "1.16.1"
dependencies:
- name: Service-1
repository: "oci://YOUR_ACR_NAME.azurecr.io/charts"
version: 0.0.1
- name: Service-2
repository: "oci://YOUR_ACR_NAME.azurecr.io/charts"
version: 0.0.2
Likt som vi gjør i chartene så må vi også huske at deployment-filene våre skal hente ut images fra ACR med rett versjonsnr! Vi kobler versjonsnr til image til versjonsnr til chart siden disse blir oppdatert samtidig. Da sørger vi for at containers-feltet på chartet ser slik ut:
containers:
- name: "NAVN PÅ CONTAINER"
image: "oci://YOUR_ACR_NAME.azurecr.io"/:"
Det siste vi da ønsker å gjøre er å gi selve hovedchartet et byggnr på samme måte som vi gjøre med ressursene og legge til distribusjonensoppgaven i leveranselinjen vår. Vi legger til funksjonen for versjonering og bruker powershell til å endre version og appVersion på chartet. Deretter legger til en HelmDeploy oppgave:
task: HelmDeploy@0
displayName: Deploy Services
inputs:
connectionType: 'Azure Resource Manager'
azureSubscription: SERVICE CONNECTION
azureResourceGroup: NAVN PÅ RESSURSGRUPPE
kubernetesCluster: NAVN PÅ K8 CLUSTER
useClusterAdmin: true
namespace: 'NAVN PÅ NAMESPACE'
command: 'upgrade'
install: true
arguments: '--debug --create-namespace'
chartType: 'FilePath'
chartPath: STI TIL HOVEDCHART
releaseName: 'Service Release'
valueFile: STI TIL GLOBAL VALUES FIL
På denne måten kan vi “enkelt” styre hvilke versjoner som skal distribueres ut til et cluster med granulær kontroll helt fra toppen av hovedchartet og helt ned til image fra Docker. Ved å utnytte mulighetene som Azure Devops har for kontinuerlig integrasjon vil vi automatisk oppdatere kodebasen ved nye bygg. Vi bruker ACR til å lagre alt som trengs for å vedlikeholde systemet og er derfor bare avhengig av å definere hva ressursene heter i ACR og hvilken versjon de har for å produksjonssette kode til kubernetes. Med alt dette på plass har vi laget følgende struktur for CI/CD løp av mikrotjenester som forhåpentligvis gjør det litt lettere å ha kontroll når størrelsen på et system øker!
Vil du være en del av et miljø der vi bygger hverandre opp, deler kunnskap og heier på hverandre? Vi søker flere kollegaer til å bli en del av gjengen!