Kubernetes-kluster med låg overhead som driftsätts vid behov på CI Worker Nodes med KIND
Michael Vittrup Larsen
Michael is a Consultant at our office in Aarhus. He has an MScEE from Aalborg University where he specialized in digital signal processing. Before joining us Michael was working on building and optimizing cloud infrastructure for the telecoms sector. In his spare time, he likes to explore the wilderness on his mountain bike.
Så testar du Kubernetes-artefakter som Helm charts och YAML-manifest i dina CI-pipelines med ett Kubernetes-kluster med låg overhead som distribueras vid behov med KIND – Kubernetes in Docker.Containers har blivit mycket populära för att paketera applikationer eftersom de löser problemet med beroendehantering. En applikation som paketeras i en container innehåller alla nödvändiga beroenden vid körning och blir därmed portabel mellan olika exekveringsplattformar. Med andra ord: om den fungerar på min dator fungerar den med stor sannolikhet även på din.
Automatiserad testning är en självklar del av DevOps, och vi bör containerisera våra tester av exakt samma skäl som vi containeriserar våra applikationer: om ett visst test ger tillförlitliga resultat på min dator bör det fungera lika bra på din, oavsett vilka bibliotek och verktyg du har installerat lokalt.
Testning med containers
Figuren nedan visar en pipeline (eller kanske två, beroende på hur du organiserar dina pipelines) där den övre delen bygger och paketerar applikationen i en container, medan den nedre delen gör samma sak med testerna som ska användas för att validera applikationen. Applikationen flyttas vidare först när de containerbaserade testerna har godkänts.
Om vi utgår från att applikationen är en nätverksansluten tjänst där black box-testning kan utföras via nätverksanslutning, är en konfiguration som den ovan enkel att implementera genom att:
Bygga applikations- och testcontainers, till exempel med ‘docker build …’
Starta en instans av applikationscontainern som är ansluten till ett nätverk, till exempel med ‘docker run …’
Starta en instans av testcontainern som är ansluten till samma nätverk som applikationen, till exempel med ‘docker run …’
Testcontainerns exit-kod avgör resultatet av applikationstestet
Detta visas i figuren nedan.
Steg 2 till 4 ovan kan också beskrivas i en docker-compose-definition med två tjänster, till exempel (testcontainern konfigureras med applikationens nätverksplats via en miljövariabel):
version: '3.7' services: test: image: test-container:latest environment: APPLICATION_URL: http://application:8080 depends_on: - application application: image: application:latest ports: - 8080:8080Ett test som använder de två containrarna kan nu köras med:
docker-compose up --exit-code-from test
Testa Kubernetes-artefakter i CI-pipelinen
Processen som beskrivs ovan fungerar bra för tester på containernivå. Men vad händer om CI-pipelines utdataartefakter omfattar Kubernetes-artefakter, till exempel YAML-manifest eller Helm charts, eller om de behöver distribueras till ett Kubernetes-kluster för att valideras? Hur testar vi i sådana situationer?
Ett alternativ är att ha ett Kubernetes-kluster distribuerat som CI-pipelines kan distribuera till. Det medför dock några frågor att ta hänsyn till:
Ett delat kluster som alla CI-pipelines kan distribuera till blir i praktiken ett multi-tenant-kluster, vilket kan kräva noggranna överväganden kring isolering, säkerhet och robusthet.
Hur dimensionerar vi Kubernetes-klustret för CI? Klustrets kapacitet kommer sannolikt att vara frikopplad från CI-worker-kapaciteten, det vill säga att de inte kan dela beräkningsresurser. Det leder till låg resursutnyttjandegrad. Vi kan heller inte dimensionera CI-klustret för litet, eftersom vi inte vill att tester ska misslyckas på grund av att andra pipelines tillfälligt förbrukar resurser.
Vi kanske vill testa våra Kubernetes-artefakter mot många versioner och konfigurationer av Kubernetes, vilket i praktiken innebär att vi behöver N CI-kluster tillgängliga.
Vi kan också skapa ett Kubernetes-kluster vid behov för varje CI-jobb. Det kräver:
Åtkomst till en molnliknande plattform där vi dynamiskt kan etablera Kubernetes-kluster.
Att våra CI-pipelines har nödvändiga behörigheter för att skapa infrastruktur, vilket kan vara oönskat ur ett säkerhetsperspektiv.
I vissa testscenarier behöver vi ett produktionsliknande kluster och måste överväga någon av lösningarna ovan, till exempel för egenskapstester eller skalbarhetstester. I många situationer kan dock de tester som vi vill att våra CI-pipelines ska utföra hanteras inom kapaciteten hos en enda CI-worker-nod. Nästa avsnitt beskriver hur du skapar kluster vid behov på en CI-worker-nod med stöd för containers.
Privat Kubernetes-kluster vid behov med KIND
Kubernetes-in-Docker (KIND) är en implementering av ett Kubernetes-kluster som använder Docker-in-Docker-teknik (DIND). Docker-in-Docker innebär att vi kan köra containers inuti containers, och att dessa inre containers endast är synliga inuti den yttre containern. KIND använder detta för att implementera ett kluster genom att använda den yttre containern för att implementera Kubernetes-klusternoder. När en Kubernetes POD startas på en nod implementeras den med containers inuti den yttre nodcontainern.
Med KIND kan vi skapa Kubernetes-kluster på begäran med flera noder ovanpå containerfunktionerna i vår CI-worker-nod.
Ett KIND Kubernetes-kluster:
Klusterkapaciteten begränsas förstås av CI-worker-nodens kapacitet, men i övrigt har Kubernetes-klustret många av funktionerna i ett produktionskluster, inklusive HA-funktioner.
Låt oss visa hur du testar en applikation som driftsatts med Helm i ett KIND-kluster. Applikationen är k8s-sentence-age, som finns på GitHub tillsammans med en GitHub Action som implementerar CI-pipelinen som beskrivs i den här bloggen. Applikationen är en enkel tjänst som kan returnera ett slumpmässigt tal (en ”ålder”) mellan 0 och 100 och även tillhandahåller relevanta Prometheus-kompatibla mätvärden.
Installera KIND
KIND är en enda körbar fil med namnet kind som i princip kommunicerar med containerruntime-miljön på CI-workern. Den skapar en (yttre) container för varje nod i klustret med hjälp av container images som innehåller Kubernetes control plane. Ett exempel på hur du installerar kind som en del av en GitHub Action finns här.
Skapa ett kluster
Med verktyget kind kan våra CI-pipelines skapa ett Kubernetes-kluster med en nod med följande kommando:
kind create cluster --wait 5m
Vi kan också skapa kluster med flera noder om vi behöver det för våra tester. Kluster med flera noder kräver en konfigurationsfil som listar nodroller:
# config.yaml kind: Cluster apiVersion: kind.x-k8s.io/v1alpha4 nodes: - role: control-plane - role: worker - role: workerMed konfigurationsfilen ovan kan vi skapa ett kluster med tre noder med följande kommando:
kind create cluster --config config.yaml
Vi kan ange vilken container image som KIND Kubernetes-noderna ska använda och därmed styra Kubernetes-versionen:
kind create cluster --image "kindest/node:v1.16.4"
På så sätt kan vi enkelt testa kompatibilitet med flera Kubernetes-versioner som en del av vår CI-pipeline.
Bygga applikationsimages och göra dem tillgängliga för KIND
Exempelapplikationen k8s-sentences-age paketeras i en container med namnet ”age”, och testerna för applikationen paketeras i en container med namnet ”age-test”. Dessa containrar byggs på vanligt sätt enligt följande:
docker build -t age:latest ../appdocker build -t age-test:latest .
Vi kan göra den nya versionen av dessa images tillgänglig för våra KIND Kubernetes-noder med följande kommando:
kind load docker-image age:latestkind load docker-image age-test:latest
När images laddas till KIND-klusternoder kopieras image-filen till varje nod i klustret.
Köra ett test
Vår pipeline driftsätter applikationen med dess Helm chart och kör testerna mot den driftsatta applikationsinstansen.
När du driftsätter applikationen med dess Helm chart testar du inte bara applikationscontainern när den driftsätts i Kubernetes, utan validerar också själva Helm chartet. Helm chartet innehåller YAML-manifesten som definierar applikationens Kubernetes-ritning, och det är särskilt viktigt att validera detta – inte bara mot olika Kubernetes-versioner utan även i olika konfigurationer, till exempel permutationer av värden som anges för Helm chartet.
Vi installerar applikationen med följande Helm-kommando. Observera att vi åsidosätter Helm chartets standardinställningar för image repository, tagg och pullPolicy så att den lokala image-filen används.
helm install --wait age ../helm/age \--set image.repository=age \--set image.tag=latest \--set image.pullPolicy=Never
Testcontainern driftsätts med hjälp av en Kubernetes Job-resurs. Kubernetes Job-resurser definierar arbetslaster som körs till slutförande och rapporterar slutförandestatus. Jobbet använder den lokala containerimagen ”age-test” som vi byggde tidigare och ansluter till applikationens POD:ar med hjälp av URL:erna som anges i miljövariabler. URL:erna refererar till Kubernetes-tjänsten som skapats av Helm-diagrammet.
apiVersion: batch/v1 kind: Job metadata: name: component-test spec: template: metadata: labels: type: component-test spec: containers: - name: component-test image: age-test imagePullPolicy: Never env: - name: SERVICE_URL value: http://age:8080 - name: METRICS_URL value: http://age:8080/metrics restartPolicy: Never </code>Jobbet driftsätts med följande kommando:
kubectl apply -f k8s-component-test-job.yaml
Kontrollera testresultatet
Vi måste vänta tills komponenttestjobbet har slutförts innan vi kan kontrollera resultatet. Verktyget kubectl kan vänta på olika villkor för olika resurser, inklusive slutförda jobb. Det innebär att vår pipeline väntar på att testet ska slutföras med följande kommando:
kubectl wait --for=condition=complete \--timeout=1m job/component-test
Komponenttestjobbet har testresultat i sina loggar. För att inkludera resultaten i pipelinens utdata skriver vi ut jobbets loggar med kubectl och använder en etikettväljare för att välja jobbets pod.
kubectl logs -l type=component-test
Komponenttestets övergripande status läses från jobb-POD:ens fält .status.succeeded och lagras i variabeln SUCCESS, som visas nedan. Om statusen visar ett fel avslutas pipelinen med ett felmeddelande:
SUCCESS=$(kubectl get job component-test \-o jsonpath='{.status.succeeded}')if [ $SUCCESS != '1' ]; then exit 1; fiecho "Component test successful"
Hela pipelinen finns i repot k8s-sentences-age på GitHub.
Det är värt att notera att det är precis detta en helm test gör: startar ett testjobb och validerar resultatet. Helm test är ett sätt att formellt integrera tester i Helm-diagram så att användare av diagrammet kan köra testerna efter att ha installerat det. Därför är det klokt att inkludera testerna i dina Helm-diagram och göra testcontainern tillgänglig för användarna av Helm-diagrammet. För att inkludera testjobbet ovan i Helm-diagrammet behöver vi bara lägga till annoteringen som visas nedan och inkludera YAML-filen som en del av diagrammet.
...metadata: name: component-test annotations: "helm.sh/hook": testNär ett KIND-kluster inte räcker till
I vissa situationer är ett lokalt Kubernetes-kluster på en CI-arbetare kanske inte det bästa för dina testbehov. Det kan till exempel vara när:
Enhetstester anropar funktioner, till exempel använder klasser från applikationen. I det här fallet är applikationen och testerna sannolikt en enda container som kan köras utan Kubernetes.
Komponenttester inte omfattar några Kubernetes-relaterade artefakter. Om exemplet ovan inte hade haft ett Helm-diagram att testa hade docker-compose-lösningen varit tillräcklig.
Tester omfattar ett egenskapstest, till exempel mätning av applikationens prestanda och skalbarhet. I sådana situationer behöver du infrastruktur med stabilare kapacitet.
Integrationstester som är beroende av andra artefakter inte enkelt kan driftsättas i det lokala KIND-klustret, till exempel en stor databas med kunddata.
Funktionella tester, integrations- eller acceptanstester kräver att hela ”applikationen” driftsätts. Vissa applikationer kanske inte ryms inom KIND-klustrets begränsade storlek.
Tester har externa beroenden, till exempel molnleverantörsspecifik ingress/load balancing, lagringslösningar, nyckelhanteringstjänster och så vidare. I vissa fall kan dessa simuleras genom att exempelvis driftsätta en databas i KIND-klustret, medan det i andra fall inte är möjligt.
Det finns dock fortfarande många fall där testning med ett KIND Kubernetes-kluster är idealiskt, till exempel när du har Kubernetes-relaterade artefakter att testa, som ett Helm-diagram eller YAML-manifest, och när ett externt CI-/staging-Kubernetes-kluster medför för mycket underhållsarbete eller använder resurser för ineffektivt.
- Cloud
- CI/CD
Subscribe to our newsletter
Related blogs