Et begynnerkurs i GitHub Actions, hvor du lærer å skrive en CI/CD-pipeline som bygger, tester og deployer en applikasjon.
Flere ønsker å lære mer om CI/CD-verktøy generelt, og GitHub Actions spesielt. Derfor er dette kurset laget, hvor man ser på CI/CD fra A til Å, med utgangspunkt i GitHub Actions.
Dette er et begynnerkurs i GitHub Actions, og passer for deg som har jobbet lite eller ingenting med dette fra før. Her starter du med en eksempelapplikasjon, og i løpet av 2 timer får du prøve deg på å skrive en CI/CD-pipeline i GitHub Actions som bygger, tester og deployer applikasjonen. I tillegg inneholder repoet en kort presentasjon av de viktigste temaene innen CI/CD generelt.
Før du kan kjøre applikasjonen lokalt, trenger du å installere .NET 6.0 SDK, og et passende verktøy for å editere kode. Hvis du ikke har noen spesielle preferanser, er Visual Studio Code et greit valg.
For å klone koden, trenger du Git. I tillegg trenger du en konto på GitHub for å kunne bruke Actions.
Når vi kommer så langt i kurset at man skal begynne å installere applikasjonen i forskjellige miljøer, trenger du Docker eller Podman for å bygge containere, og kubectl for å orkestrere containerne du bygger i Kubernetes.
For at applikasjonene vi lager skal kunne brukes av noen andre enn oss, må vi typisk få de ut et eller annet sted hvor noen andre enn oss kan bruke de. Dette andre stedet kaller vi gjerne produksjon, og på veien ut kan det skje mye rart.
Spørsmål: Hva må til for at en applikasjon vi har laget kommer seg ut i produksjon?
- CI: Kontinuering Integrering (Continuous Integration). Praksis hvor man forsøker å samle kode-endringer fra flere bidragsytere hyppig, ved å automatisk merge inn og teste små endringer kontinuerlig.
- CD: Kontinuerlig Leveranse (Continuous Delivery). Tilnærming hvor man blant annet ønsker å redusere risiko for alvorlige feil i produksjon, ved å levere hyppige små endringer.
- CI/CD-pipeline: Den automatiske prosessen kode må igjennom for å komme seg fra en utvikler til produksjon.
- Ressurs: Noe som applikasjonen vår er avhengig av for å kjøre. Noen typiske eksempler er: En datamaskin (eller tilsvarende) som kan kjøre den ferdige applikasjonen vår. Databaser, køer og filer for å holde på tilstand. Nettverk for å snakke med omverdenen. Andre applikasjoner som vår applikasjon bruker.
- Delte ressurser: Ressurser som brukes av andre applikasjoner enn den applikasjonen vi jobber med. Dette eksempelvis være delte servere, nettverk, byggesystemer, API, med mer.
- Miljø: En samling av alle ressursene som trengs for å kjøre applikasjonen vi jobber med. Det miljøet som brukerne av applikasjonen bruker, kalles typisk produksjon. Andre viktige miljøer er testmiljøer, som brukes for å sjekke at nye versjoner av applikasjonen fungerer før de installers i produksjonsmiljøet, og lokalt miljø, som brukes av utviklere for å sjekke at applikasjonen fungerer mens man utvikler.
- Infrastruktur: Samlingen av alle ressursene og miljøene en applikasjon bruker.
- Infrastruktur som Kode: En tilnærming hvor man automatiserer oppsett av infrastruktur ved å uttrykke den som kode.
Spørsmål: Har du hørt noen andre rare CI/CD-ord du lurer på hva betyr? Er det forklaringer i ordlisten over du er uenig i?
Actions er et verktøy for å bygge CI/CD-pipelines. Det er tilgjengelig direkte i GitHub, og settes opp ved at man legger inn spesielle YAML-filer i mappen .github/workflows
.
Hver fil definerer en workflow. En workflow er en automatisk prosess som vi ønsker at skal kjøre når en spesiell hendelse skjer. Dette kan eksempelvis være at man bygger og tester koden automatisk når det kommer inn nye endringer i en pull-request, eller at man bygger, tester og installerer koden i et miljø når nye endringer kommer inn på main-branchen i repoet.
Hendelsene som starter en workflow kaller vi en event. Dette er ofte hendelser knyttet til koderepoet, f.eks. at ny kode kommer inn på en spesiell branch, eller at noe nytt skjer i en pull-request, men det kan også være at man manuelt kan lage en event, eller at man kan trigge eventer jevnlig, for f.eks. å oppdatere pakker som applikasjonen er avhengig av.
Hver workflow består av en eller flere jobber, som igjen kan bestå av ett eller flere steg. Eksempelvis kan man ha en jobb som bygger applikasjonen, som igjen innholder ett steg som installerer pakker som applikasjonen trenger, og ett steg som kjører en kommando for å bygge koden.
For å gjøre det lettere å lage worksflows som trenger å gjennomføre komplekse, men mye brukte oppgaver, kan man bruke actions. Det finnes mange ferdige actions man kan bruke, og man kan lage sine egne actions hvis man trenger det.
Når en workflow skal kjøre, trenger den et sted å kjøre. Dette kaller man runners. GitHub leverer noen ferdige runnere som man kan bruke, og det går også ann å sette opp sine egne.
Start med å forke repoet github-actions-101, sånn at du får en kopi av repoet knyttet til din egen GitHub-bruker. Deretter er det bare å klone din kopi av repoet lokalt på maskinen din.
Lag en fil som heter hello-actions.yml
under mappen .github/workflows
. Lim in koden under, og sjekk den inn. Gå til Actions-fanen i din klone av "github-actions-101"-repoet, og se hva som skjer her.
name: "Hello Actions!"
on:
workflow_dispatch:
push:
jobs:
say-hello:
runs-on: ubuntu-latest
steps:
- name: "Echo Greeting"
run: "echo 'Hei på deg GitHub Actions!'"
- name: "Echo Goodnight"
run: "echo 'Natta!'"
YAML-koden over, definerer en workflow med navnet "Hello Actions!". Den er satt opp til å trigge (on:
) når en av to eventer skjer: Enten hvis workflowen blir startet manuelt fra grensesnittet til GitHub (workflow_dispatch:
), eller hvis det pushes en commit til koderepoet (push:
).
Når workflowen kjører, starter den en jobb som heter say-hello
. Denne jobben kjører på en maskin som har en nyere versjon av operativsystemet Ubuntu installert. Selve jobben består av to steg. Det første steget heter "Echo Greeting", og det bruker kommandoen echo til å printe teksten Hei på deg GitHub Actions! til terminalen. Det andre steget ligner veldig på det første, men printer i stedet teksten Natta!.
Oppgave: Klarer du å utvide workflowen over med ett steg til som kjører en annen valgfri kommando?
Tips: Når du er ferdig med denne oppgaven, kan det være lurt å fjerne push:
-triggeren, så workflowen "Hello Actions!" ikke kjører hele tiden resten av kurset.
Nå skal vi ta i bruk GitHub Actions til å lage en enkel workflow som bygger og tester Sticky Notes-applikasjonen som finnes i dette repoet. En slik workflow er ofte et viktig steg i en større CI/CD-pipeline, og den kjøres gjerne før man merger inn ny kode i repoet, ofte som en del av en pull request prosess. Målet er å finne ut om applikasjonen fremdeles bygger og ser ut til å fungere som den skal.
Siden CI/CD-pipelines i stor grad bare er en litt avansert skript som kjører en serie med terminal-kommandoer, og GitHub Actions langt på vei bare er et verktøy som gjør det lettere å skrive sånne skript, er det ofte lurt å starte prosessen med å utvikle en ny workflow lokalt i sin egen terminal. Har man god oversikt over hvilke kommandoer man trenger å kjøre lokalt, for å få til det man ønsker, blir det ofte mye lettere å utvikle selve workflowen etterpå. Vi starter derfor denne seksjonen av kurset, med å se på hvordan man kan bygge og teste Sticky Notes-applikasjonen lokalt.
Sticky Notes er en applikasjon som er utviklet med .NET, så derfor kommer vi til å bruke terminal-programmet .NET CLI for å installere avhengigheter, bygge og teste applikasjonen.
Man kan bruke kommandoen dotnet restore
for å installere avhengigheter i .NET-applikasjoner. Disse avhengighetene, sammen med annen bygg-relevant informasjon, er satt opp i prosjektfiler, som har fil-endelsen .csproj
. Vi kan kjøre kommandoen under for å restore alle avhengighetene i test-prosjektet Notes.Api.Test
.
github-actions-101$> dotnet restore Notes.Api.Test/Notes.Api.Test.csproj
Determining projects to restore...
Restored /github-actions-101/Notes.Api/Notes.Api.csproj (in 220 ms).
Restored /github-actions-101/Notes.Api.Test/Notes.Api.Test.csproj (in 257 ms).
Legg merke til hvordan vi også restoret avhengighetene i selve API-prosjektet Notes.Api
. Dette skjedde fordi test-prosjektet Notes.Api.Test
er avhengig av Notes.Api
-prosjektet.
For å bygge test-prosjektet bruker vi kommandoen dotnet build
. Her har vi også lyst til å bruke flagget --configuration Release
, som forteller .NET CLI at vi har lyst til å bygge optimalisert for kjøring/release i et miljø, og ikke for debugging, som typisk er tilfellet når vi utvikler lokalt. I tillegg har vi lyst til å bruke flagget --no-restore
, som forteller .NET CLI at vi ikke trenger å sjekke om det må installeres noen avhengigheter først, siden vi akkurat restoret prosjektet.
github-actions-101$> dotnet build --no-restore --configuration Release Notes.Api.Test/Notes.Api.Test.csproj
Microsoft (R) Build Engine version 17.0.1+b177f8fa7 for .NET
Copyright (C) Microsoft Corporation. All rights reserved.
Notes.Api -> /.../github-actions-101/Notes.Api/bin/Release/net6.0/Notes.Api.dll
Notes.Api.Test -> /.../github-actions-101/Notes.Api.Test/bin/Release/net6.0/Notes.Api.Test.dll
Build succeeded.
0 Warning(s)
0 Error(s)
Time Elapsed 00:00:02.37
Igjen ser vi at vi ved å bygge Notes.Api.Test
også bygget Notes.Api
, fordi test-prosjektet er avhengig av selve API-prosjektet.
For å kjøre selve testene bruker vi kommandoen dotnet test
. Igjen skal vi bruke flagget --configuration Release
, og i tillegg flagget --no-build
, som forteller test-kommandoen at man ikke trenger å sjekke om test-prosjektet må bygges før testene kjører.
github-actions-101$> dotnet test --no-build --configuration Release Notes.Api.Test/Notes.Api.Test.csproj
Test run for /.../github-actions-101/Notes.Api.Test/bin/Release/net6.0/Notes.Api.Test.dll (.NETCoreApp,Version=v6.0)
Microsoft (R) Test Execution Command Line Tool Version 17.0.0+68bd10d3aee862a9fbb0bac8b3d474bc323024f3
Copyright (c) Microsoft Corporation. All rights reserved.
Starting test execution, please wait...
A total of 1 test files matched the specified pattern.
Passed! - Failed: 0, Passed: 9, Skipped: 0, Total: 9, Duration: 265 ms - /.../github-actions-101/Notes.Api.Test/bin/Release/net6.0/Notes.Api.Test.dll (net6.0)
Ni tester passerte! Neppe den mest omfattende test-suiten, men godt nok for det vi skal gjøre i dag.
Kommandoen dotnet test
kan kjøres med flere andre nyttige flagg. Eksempelvis kan flagget --verbosity
brukes for å styre hvor mye informasjon testene skal printe til terminalen, og flagget --logger
kan brukes for å få generert en fil som inneholder testresultatene (dette kan vise seg å være nyttig senere).
github-actions-101$> dotnet test \
--no-build \
--configuration Release \
--verbosity normal \
--logger trx \
Notes.Api.Test/Notes.Api.Test.csproj
Build started 10/18/2022 8:43:17 PM.
Test run for /github-actions-101/Notes.Api.Test/bin/Release/net6.0/Notes.Api.Test.dll (.NETCoreApp,Version=v6.0)
Microsoft (R) Test Execution Command Line Tool Version 17.0.0+68bd10d3aee862a9fbb0bac8b3d474bc323024f3
Copyright (c) Microsoft Corporation. All rights reserved.
Starting test execution, please wait...
A total of 1 test files matched the specified pattern.
Results File: /github-actions-101/Notes.Api.Test/TestResults/_HAL_2022-10-18_20_43_20.trx
Passed! - Failed: 0, Passed: 9, Skipped: 0, Total: 9, Duration: 268 ms - /.../github-actions-101/Notes.Api.Test/bin/Release/net6.0/Notes.Api.Test.dll (net6.0)
Build succeeded.
0 Warning(s)
0 Error(s)
Time Elapsed 00:00:02.21
Etter at du har kjørt kommandoen, kan du ta en titt på .trx
-filen som ble generert i mappen /github-actions-101/Notes.Api.Test/TestResults/
. Mye XML her, men kanskje det finnes noe som kan vise frem innholdet av filen på en fin måte?
Tips: På noen plattformer kan man bryte ned lange kommandoer over flere linjer ved å bruke \
på slutten av hver linje. Dette kan gjøre kommandoene lettere å lese. Hvis dette ikke fungerer i din terminal, kan du bare gjøre om kommandoen i eksempelet over til en lang linje.
Nå som vi har god oversikt over hva som skal til for å restore, bygge og teste en .NET-applikasjon, gjenstår det bare å skrive en GitHub Actions workflow som gjør det samme som vi akkurat har gjort i terminalen.
Du kan ta utgangspunkt i workflowen under, ved å legge den inn i en fil som f.eks. heter hello-dotnet.yml
under mappen .github/workflows
.
name: "Hello .NET"
on:
workflow_dispatch:
jobs:
build-test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: "Setup .NET Core SDK"
uses: actions/setup-dotnet@v2
with:
dotnet-version: "6.0.x"
- name: "Print .NET CLI version"
run: dotnet --version
Denne workflowen inneholder allerede en jobb som har tre steg:
- actions/checkout er en ferdig action, som sjekker ut koden vi har i repoet på maskinen jobben kjører på. Dette er en mye brukt action!
- actions/setup-dotnet installerer .NET CLI på maskinen, sånn at kommandoen
dotnet
er tilgjengelig for de neste stegene i jobben. - Til slutt kjører vi et steg som viser at vi har .NET CLI tilgjengelig ved å kjøre kommandoen
dotnet --version
.
Oppgave 1: Legg til tre steg i workflowen over som restorer, bygger og kjører testene i Notes.Api.Test
.
Oppgave 2: Man kan bruke miljøvariabler i GitHub Actions til f.eks. å samle verdier som brukes flere steder i en workflow. Klarer du å lage en miljøvariabel med banen til test-prosjektet (Notes.Api.Test/Notes.Api.Test.csproj
) som kan gjenbrukes i de forskjellige stegene?
Oppgave 3: dorny/test-reporter
er en ferdig action som kan brukes til å vise frem testresultater. Klarer du å legge til et steg på slutten av jobben som bruker denne action til å vise frem .trx
-testresultatene som dotnet test
produserer?
Tips: Hvis du står fast på en av oppgavene over, er det bare å be om hjelp, eller ta en titt på eksempel-løsningen her.
Info: Man kan bruke GitHub Actions Marketplace for å finne flere ferdige actions når du skal skrive dine egne workflows senere, men man trenger ikke flere for å løse oppgavene over.
Vi skal etter hvert lage en workflow som deployer Notes.Api
med Docker og Kubernetes (ofte bare kalt k8s). Det betyr at vi må se litt på hvordan man bruker disse verktøyene. Først ut er Docker (eller Podman)!
NB: Eksemplene under bruker kommandoen docker
, så hvis du har valgt å installere podman
, må du bruke podman
i stede for docker
når du kjører eksemplene.
Docker er en teknologi som lar oss pakke sammen programmer og en forenklet virtuell datamaskin til noe man kaller en container. Det er litt som at man i stede for å levere et dataprogram som må installeres på en annen maskin før man kan bruke det, leverer dataprogrammet ferdig installert på en datamaskin.
Container-teknologi, som Docker er et eksempel på, har flere fordeler:
- Man kan utvikle applikasjoner med mange forskjellige teknologier, men ved å putte de i en container, kan alle de forskjellige applikasjonene kjøres med samme teknologi, som bare trenger å vite hvordan man kjører en container.
- Man får større kontroll over datamaskinen applikasjonen kjører på i alle miljøer, siden denne datamaskinen i stor grad er pakket sammen med applikasjonen.
- Sammenlignet med tradisjonelle virtuelle datamaskiner (eller vanlige datamaskiner for den saks skyld), er containere mye enklere å overføre mellom forskjellige datamaskiner, og er lettere å bruke i en CI/CD-pipeline.
For å lage en container, trenger man en mal. Denne malen kalles et image, og representerer et øyeblikksbilde av containeren etter at alle stegene i en gitt oppskriften er fulgt. Oppskriften kaller man gjerne en Dockerfile. En Dockerfile kan f.eks. inneholde stegene:
- Start med et image Microsoft har laget, som inneholder operativsystemet Ubuntu med .NET CLI ferdig installert.
- Kopier inn koden til applikasjonen vår, og bygg denne med .NET CLI.
- Start den ferdig bygde applikasjonen vår.
Hvis man bygger et image fra denne oppskriften, vil man ende opp med et image som inneholder en spesifikk versjon av applikasjonen vår, basert på den koden som ble kopiert inn i imaget da oppskriften ble kjørt.
Sagt på en annen måte: Dockerfilen er en oppskrift som forteller oss hvordan man lager/byger et image. Hver kan man følger oppskriften, ender man opp med et image, som representerer en versjon av applikasjonen vår. Imaget kan man så senere dele og bruke til å lage containere.
Før vi skriver vårt eget image for applikasjonen Notes.Api
, kan det være nyttig å leke litt med et ferdiglaget image. Et litt morsomt image å leke med, er wernight/funbox. Dette imaget lager en container som inneholder et Linux operativsystem, hvor det er installert flere artige kommandoer som printer ut morsom tekst til terminalen. En slik kommando er sl
. For å kjøre denne kommandoen i containeren som bygges av wernight/funbox-imaget, kan vi kjøre følgende kommando:
$> docker run -it wernight/funbox:latest sl
Hva skjedde her?
docker run -it
forteller Docker at vi har lyst til å kjøre containeren vi lager fra imaget i "interactive mode", og at vi har lyst til å få vist frem det som printes til terminalen i containeren i vår egen terminal.wernight/funbox:latest
er en referanse til et image.wernight
er den som har publisert dette imaget,funbox
er navnet på imaget oglatest
er versjonen. Denne referansen til et image kalles ofte en image tag eller bare tag.- Imaget vi lager containeren fra, forventer at vi sender med kommandoen vi vil kjøre i containeren som et argument. Derfor er det siste argumentet over
sl
, som er kommandoen vi ønsker å kjøre inne i containeren.
Oppgave: Ta en titt på de andre kommandoene som finnes i wernight/funbox og prøv de ut! Hvis terminalen blir rotete, kan du tømme den med kommandoen clear
.
Akkurat som når man skal skrive en workflow, er det nyttig å vite hvilke kommandoer man trenger å kjøre lokalt for å bygge og starte applikasjonen man skal skrive en Dockerfile for. Det å skrive en Dockerfile er nemlig ikke helt forskjellig fra å skrive skript det heller. Siden vi ønsker at containeren vår skal bygge og kjøre Notes.Api
, starter vi med å ta en titt på hvordan man gjør dette lokalt med .NET CLI.
Når man skal bygge .NET-applikasjoner for leveranse og installasjon på andre maskiner, bruker man kommandoen dotnet publish
i stede for dotnet build
. I tillegg til blant annet å bygge applikasjonen, pakker denne kommandoen sammen alle filene den ferdig bygde applikasjonen består av, og putter de i en mappe, klare for å leveres og kjøres på en datamaskin.
github-actions-101$> dotnet publish --output ./Notes.Published --configuration Release --self-contained false Notes.Api/Notes.Api.csproj
Microsoft (R) Build Engine version 17.0.1+b177f8fa7 for .NET
Copyright (C) Microsoft Corporation. All rights reserved.
Determining projects to restore...
All projects are up-to-date for restore.
Notes.Api -> /.../github-actions-101/Notes.Api/bin/Release/net6.0/Notes.Api.dll
Notes.Api -> /.../github-actions-101/Notes.Published/
Kommandoen over publiserer Notes.Api
til en mappe som heter Notes.Published
. Akkurat som når vi kjørte testene, bruker vi flagget --configuration Release
for å fortelle dotnet publish
at vi ønsker å bygge applikasjonen for leveranse.
Vi har i tillegg med flagget --self-contained false
, men å gå inn på hva dette betyr, er litt utenfor scope i denne omgangen. De som er spesielt interesserte kan ta en titt på denne oversikten.
Nå kan vi starte den ferdig publiserte applikasjonen direkte med dotnet
. Naviger ned i mappen Notes.Published
og kjør følgende kommando:
github-actions-101$> cd Notes.Published/
github-actions-101/Notes.Published$> dotnet Notes.Api.dll
info: Microsoft.Hosting.Lifetime[14]
Now listening on: http://localhost:5000
info: Microsoft.Hosting.Lifetime[14]
Now listening on: https://localhost:5001
info: Microsoft.Hosting.Lifetime[0]
Application started. Press Ctrl+C to shut down.
info: Microsoft.Hosting.Lifetime[0]
Hosting environment: Development
info: Microsoft.Hosting.Lifetime[0]
Content root path: /.../github-actions-101/Notes.Published/
info: Microsoft.Hosting.Lifetime[0]
Hvis alt gikk bra, kan du åpne http://localhost:5000/client og leke litt med Sticky Notes-applikasjonen. Du kan også pinge APIet direkte ved å gå til http://localhost:5000/ping.
Start med å opprette en fil i mappen Notes.Api
med navnet Dockerfile
, og fyll den med følgende innhold:
FROM mcr.microsoft.com/dotnet/sdk:6.0 AS build-stage
WORKDIR /Sources
COPY /Notes.Api ./
RUN dotnet publish --output ./Notes.Published --configuration Release --self-contained false
FROM mcr.microsoft.com/dotnet/aspnet:6.0
WORKDIR /Application
COPY --from=build-stage /Sources/Notes.Published ./
ENTRYPOINT ["dotnet", "Notes.Api.dll"]
Her tar vi utgangspunkt i et image fra Microsoft som inneholder et Linux-basert operativsystem, med .NET SDK v6.0 ferdig installert FROM mcr.microsoft.com/dotnet/sdk:6.0
. Videre sier vi at vi skal jobbe i en mappe som heter /Sources
inne i containeren vi bygger.
Deretter går vi videre med å kopiere alle filene fra mappen /Notes.Api
inn i imaget, før vi publiserer applikasjonen med dotnet publish
.
Helt på slutten bruker vi ENTRYPOINT
for å si at når man starter en container basert på dette imaget, så skal vi kjøre den ferdig bygde Notes.Api.dll
med dotnet
direkte, men før dette skjer det noe rart. Hvorfor har vi en ny runde med FROM
og WORKDIR
?
Dette er et eksempel på det som kalles et multi-stage bygg. I multi-stage bygg, bygger vi flere imager på rad. Her bygger vi først et image med .NET SDK v6.0, som bare brukes til å publisere Notes.Api
. Deretter går vi rett videre å bygger et nytt image basert på ASP.NET-runtime imaget til Microsoft. Dette imaget er spesialtilpasset for å kjøre ASP.NET Core applikasjoner, som er det rammeverket Notes.Api
er bygget med. Fra det første imaget tar vi også bare med oss de ferdig publiserte filene i /Sources/Notes.Published
. Dette betyr at vi ender opp med et mye mindre image, og i et multi-stage bygg er det bare det siste imaget vi tar vare på.
Det kan kanskje virke litt rart å trekke inn en sånn optimaliseringsteknikk i et begynnerkurs, men denne teknikken er veldig vanlig, så det kan være greit å vite hva det er snakk om, hvis man støter på det i andre dockerfiler.
Oppgave: Docker cacher viktige steg når man bygger et image, derfor kan det være nyttig å skille ut operasjoner som sjelden endrer seg i egne steg i starten av dockerfilen. På den måten får man et raskere image-bygg. Klarer du å skille ut restore av Notes.Api i et eget steg før dotnet publish
, som bare kopierer inn /Notes.Api/Notes.Api.csproj
og kjører dotnet restore
på denne prosjektfilen? Hvis du står fast, er det bare å spørre om hjelp, eller ta en titt på forslaget til løsning her.
Tips: Man kan lage en .dockerignore
-fil for å begrense hvilke filer Docker kopierer inn i imaget når man bygger det. Det kan gjøre det imaget litt kjappere å bygge, og det ferdige imaget litt mindre i størrelse.
Med en Dockerfile på plass, kan vi bygge et image med docker build
. Kommandoen under bygger et image basert på filen Notes.Api/Dockerfile
, og image vi bygger tagges med notes-api:v0
, som er referansen vi kan bruke til å kjøre containere basert på imaget senere. Helt til slutt sender vi inn banen til mappen som vi ønsker at Docker skal kopiere inn filer til imaget fra. Siden vi står i rot-mappen til repoet, blir det banen til mappen vi er i, dvs. ./
.
github-actions-101$> docker build --file Notes.Api/Dockerfile --tag notes-api:v0 ./
Sending build context to Docker daemon 148.8MB
Step 1/8 : FROM mcr.microsoft.com/dotnet/sdk:6.0 AS build-stage
---> c315566c49a2
Step 2/8 : WORKDIR /Sources
---> Using cache
---> ab33d5dfbc3d
Step 3/8 : COPY /Notes.Api ./
---> d3d412c5ebb4
Step 4/8 : RUN dotnet publish --output ./Notes.Published --configuration Release --self-contained false
---> Running in 453acb5ce388
MSBuild version 17.3.2+561848881 for .NET
Determining projects to restore...
Restored /Sources/Notes.Api.csproj (in 3.92 sec).
Notes.Api -> /Sources/bin/Release/net6.0/Notes.Api.dll
Notes.Api -> /Sources/Notes.Published/
Removing intermediate container 453acb5ce388
---> 9a160daf0848
Step 5/8 : FROM mcr.microsoft.com/dotnet/aspnet:6.0
---> 914094d6a4a0
Step 6/8 : WORKDIR /Application
---> Using cache
---> 4d48e7d26353
Step 7/8 : COPY --from=build-stage /Sources/Notes.Published ./
---> 34ed4d0be017
Step 8/8 : ENTRYPOINT ["dotnet", "Notes.Api.dll"]
---> Running in 07fa0d7271c1
Removing intermediate container 07fa0d7271c1
---> eeeaf97b375e
Successfully built eeeaf97b375e
Successfully tagged notes-api:v0
Nå kan vi kjøre opp en container baser på imaget notes-api:v0
med docker run
.
$> docker run -it -p 8000:80 notes-api:v0
info: Microsoft.Hosting.Lifetime[14]
Now listening on: http://[::]:80
info: Microsoft.Hosting.Lifetime[0]
Application started. Press Ctrl+C to shut down.
info: Microsoft.Hosting.Lifetime[0]
Hosting environment: Production
info: Microsoft.Hosting.Lifetime[0]
Content root path: /Application/
Som før bruker vi flagget -it
, i tillegg bruker vi flagget -p 8000:80
for å fortelle Docker at vi ønsker at man skal sende all HTTP-trafikk fra vår maskin på port 8000 til port 80 i containeren. Hvis alt går bra, skal det være mulig å åpne http://localhost:8000/client/ og http://localhost:8000/ping, som når Sticky Notes-applikasjonen kjørte direkte på vår maskin.
Det er fint å ha et image man kan kjøre lokalt på vår maskin, men målet er å dele dette imaget, sånn at det også kan kjøre på en annen maskin. For å gjøre dette må vi laste imaget opp til et container-register.
Et container-register er i korte trekk en tjeneste som kan ta vare på og dele videre ferdig bygde imager. Man kan se litt på det som en filserver, eller et pakke-register for programvare. For å dele et image via et container-register, er det et par ting som må være på plass:
- Man må være logget på container-registrert man ønsker å bruke.
- Imaget man skal dele må være tagget på en passende måte.
- Man må pushe imaget opp til registeret med kommandoen
docker push
.
For denne workshoppen er det satt opp et container-register i Azure som heter devops101registry.azurecr.io
. Dette er registeret vi skal bruke videre i kurset.
Før vi kan pushe docker-imager til registeret devops101registry.azurecr.io
, må vi logge inn med docker
. Kjør kommandoen under, og logg på med dette brukernavnet og dette passordet.
$> docker login devops101registry.azurecr.io
Username: devops101registry
Password:
WARNING! Your password will be stored unencrypted ...
Login Succeeded
Hvis alt gikk som det skulle, skal den siste meldingen fra kommandoen være "Login Succeeded".
Får du en advarsel om at passordet kommer til å lagres ukryptert, er det bare å se bort ifra denne.
Imager som skal lastes opp til registeret vi bruker i dette kurset, må ha en tag som starter på devops101registry.azurecr.io
. I tillegg må taggen inneholde noe som er unikt for deg som kursdeltaker, da alle deltakerne sine imager havner i samme register. Husk derfor å bytte ut [DITT BRUKERNAVN]
i kommandoen under med noe passende, sånn at du ender opp med en image tag som er spesifikk for deg, f.eks. noe i retning av devops101registry.azurecr.io/tae-notes-api:v0
.
$> docker tag notes-api:v0 devops101registry.azurecr.io/[DITT BRUKERNAVN]-notes-api:v0
Da gjenstår det bare å pushe imaget opp til container-registeret med docker push
.
$> docker push devops101registry.azurecr.io/[DITT BRUKERNAVN]-notes-api:v0
The push refers to repository [devops101registry.azurecr.io/tae-notes-api]
c06c1058259d: Pushed
95f8cee92fd0: Mounted from notes-api
619c49f548ce: Mounted from notes-api
dc392f0ae18a: Mounted from notes-api
b00c9e3dc8e6: Mounted from notes-api
aa8b36ac3266: Mounted from notes-api
fe7b1e9bf792: Mounted from notes-api
v0: digest: sha256:01f9cc95675c9452ccff266f4658999f34ab6c0ef517d681ad8ef9b955091028 size: 1790
Hvis alt gitt bra, er du klar til å gå over til å se litt på Kubernetes.
Tips: Hvis du er usikker på om imaget ditt ble lastet opp til container-registeret, kan du spørre kursholder om vedkommende ser det i registeret.
Kubernetes er et populært verktøy for å kjøre containere. I tillegg til funksjonalitet for å kjøre kontainere, inneholder det masse andre ting, som muligheten til å skalere opp og ned antallet containere en applikasjon består av, verktøy for å sette opp nettverk mellom containerne, og mye mer. Dette kurset har ikke som mål å gi en grundig introduksjon til Kubernetes, til dette anbefales Team Utvikleropplevelse sitt Kubernetes-kurs nå i november. Her kommer vi bare til å dekke det helt minimale man trenger å kunne for å sette opp en GitHub Actions workflow som deployer Notes.Api
til Kubernetes.
Til dette kurset er det også satt opp et Kubernetes-cluster som heter devops-101-cluster
. Dette er clusteret vi kommer til å jobbe videre med i kurset.
For å kunne jobbe med Kubernetes-clusteret devops-101-cluster
, må vi konfigurere kubectl
. Konfigurasjonen vi trenger finner du her.
Det er flere måter man kan få kubectl
til å bruke denne konfigurasjonen på:
- Man kan lagre konfigurasjonen i en egen fil, og bruke argumentet
--kubeconfig
til å fortellekubectl
at man skal bruke denne konfigurasjonen:kubectl get pods --kubeconfig ~/devops-101-config.yaml
. Hvis man velger denne løsningen, må man huske å legge på argumentet--kubeconfig
på allekubectl
-kommandoer man kjører videre i kurset. - Man kan flette inn elementene fra
clusters
,contexts
ogusers
ikubectl
sin default konfigurasjonsfil (ofte kalt kubeconfig-filen), som er~/.kube/config
på Linux og Mac, og%USERPROFILE%\.kube\config
på Windows. Flettingen gjør man ved å åpne kubeconfig-filen i en teksteditor, og klippe inn de forskjellige delene der de hører hjemme. I tillegg må man oppdaterecurrent-context
tildevops-101-cluster
. - Man kan lagre konfigurasjonen i en egen fil, og så opprette miljøvariabelen
KUBECONFIG
, med en bane til denne (og flere) konfigurasjonsfiler. Dette er ofte den mest praktiske løsningen hvis man jobber mye med forskjellige konfigurasjoner, men kan være den mest omstendelige løsningen å sette opp.
Velg en av metodene over for å konfigurere kubectl
til å nå devops-101-cluster
. Når du har gjort dette, skal du kunne hente alle poddene som kjører i navnerommet kube-system
, og få et svar litt som vist under:
$> kubectl get pods --namespace kube-system
NAME READY STATUS RESTARTS AGE
azure-ip-masq-agent-9g24h 1/1 Running 0 14h
cloud-node-manager-ztb2l 1/1 Running 0 14h
coredns-autoscaler-5589fb5654-sknvl 1/1 Running 0 14h
coredns-b4854dd98-56fgr 1/1 Running 0 14h
coredns-b4854dd98-fzf48 1/1 Running 0 14h
csi-azuredisk-node-8cshv 3/3 Running 0 14h
csi-azurefile-node-kxl6c 3/3 Running 0 14h
konnectivity-agent-cb784597d-b55lq 1/1 Running 0 14h
konnectivity-agent-cb784597d-wxwql 1/1 Running 0 14h
kube-proxy-n45m4 1/1 Running 0 14h
metrics-server-f77b4cd8-crwl9 1/1 Running 0 14h
metrics-server-f77b4cd8-jq29q 1/1 Running 0 14h
For at Kubernetes skal skjønner hvordan man kjører Notes.Api
, må vi skrive litt Kubernetes-konfigurasjon.
Start med å opprette en fil i mappen Notes.Api
med navnet Kubernetes.yaml
, og fyll den med følgende innhold, hvor du erstatter [DITT BRUKERNAVN]
med noe passende for deg, sånn at du ender opp med f.eks. tae-notes-api
og samme image tag som du har brukt tidligere:
apiVersion: apps/v1
kind: Deployment
metadata:
name: [DITT BRUKERNAVN]-notes-api
spec:
replicas: 1
selector:
matchLabels:
app: [DITT BRUKERNAVN]-notes-api
template:
metadata:
labels:
app: [DITT BRUKERNAVN]-notes-api
spec:
imagePullSecrets:
- name: devops101registry-credentials
containers:
- name: notes-api
image: devops101registry.azurecr.io/[DITT BRUKERNAVN]-notes-api:v0
imagePullPolicy: Always
ports:
- containerPort: 80
---
apiVersion: v1
kind: Service
metadata:
name: [DITT BRUKERNAVN]-notes-api
spec:
type: LoadBalancer
selector:
app: [DITT BRUKERNAVN]-notes-api
ports:
- protocol: TCP
port: 80
Denne konfigurasjonen inneholder i hovedsak to ting. Ett deployment som forteller Kubernetes hvordan vi vil at man skal lage containere basert på imaget vårt. Her er det blant annet spesifisert at man ønsker en instans/replika av containeren, og vi forteller Kubernetes hvilke image tag man kan bruke for å hente imaget.
I tillegg setter vi opp en service, som forteller Kubernetes hvordan HTTP-trafikk til clusteret skal rutes videre til containerne som deploymenten setter opp.
Med Kubernetes-konfigurasjonen på plass, kan vi deploye Notes.Api
til Kubernetes med kubectl apply
.
$> kubectl apply --filename Notes.Api/Kubernetes.yaml
deployment.apps/[DITT BRUKERNAVN]-notes-api created
service/[DITT BRUKERNAVN]-notes-api created
Hvis kubectl apply
kjørte uten feil, kan vi se litt på hva som kom inn i clusteret. La oss først ta en titt på deploymentene i clusteret.
$> kubectl get deployments
NAME READY UP-TO-DATE AVAILABLE AGE
...
[DITT BRUKERNAVN]-notes-api 1/1 1 1 68s
Her finner man deploymenten som vi har konfigurert opp, og man kan bruke kubectl describe deployment [DITT BRUKERNAVN]-notes-api
for å få flere detaljer om den.
Deploymenten starter noe som kalles pods. Dette er hva som faktisk kjører containeren vår i Kubernetes.
$> kubectl get pods
NAME READY STATUS RESTARTS AGE
...
[DITT BRUKERNAVN]-notes-api-f5784ddbf-28cfk 1/1 Running 0 111s
Oppgave 1: Forsøk å slette podden din med kubectl delete pod [NAVN PÅ POD]
. Hva ser du når du kjører kubectl get pods
igjen?
Til slutt kan vi ta en titt på servicen som ble satt opp.
$> kubectl get services
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
kubernetes ClusterIP 10.0.0.1 <none> 443/TCP 29h
...
[DITT BRUKERNAVN]-notes-api LoadBalancer 10.0.106.98 20.76.158.49 80:31338/TCP 34s
Her kan vi se at servicen har fått en ekstern IP-adresse. Denne kan vi bruke for å nå Sticky Notes-applikasjonen som kjører på clusteret. Hvis du åpner http://[DIN EXTERNAL-IP]/client og http://[DIN EXTERNAL-IP]/ping, skal du se det samme som du så når applikasjonen kjørte lokalt.
Oppgave 2: I Kubernetes-konfigurasjonen er det replicas:
i deploymentet som styrer hvor mange instanser av Notes.Api man starter opp. Klarer du å skalere opp din versjon av Notes.Api til to instanser? Hva skjer når du forsøker å bruke Notes.Api når det er mer enn en instans som kjører?
Nå som vi kjenner litt til både Docker og Kubernetes, er vi klar til å lage en GitHub Actions workflow som:
- Bygger et image fra
Notes.Api/Dockerfile
, og pusher det opp til container-registeretdevops101registry.azurecr.io
, med en image-tag som er unik for denne kjøringen av workflowen. - Oppdaterer
Notes.Api/Kubernetes.yaml
med taggen til det ny-byggede imaget. - Deployer den oppdaterte Kubernetes-konfigurasjonen til
devops-101-cluster
- Sånn at Kubernetes kan hente ned det nye imaget fra container-registeret, og kjøre opp applikasjonen vår.
Actions kan hente hemmeligheter som er konfigurert under Settings -> Actions -> Repository secrets. For at workflowen vi lager skal kunne logge seg på container-registeret og kubernetes-clusteret, trenger vi derfor å sette opp de samme hemmelighetene her som vi har brukt ellers i kurset.
Disse hemmelighetene vil vi da kunne hente ut i en workflow med syntaksen ${{ secrets.CONTAINER_REGISTRY_USERNAME }}
, ${{ secrets.CONTAINER_REGISTRY_PASSWORD }}
og ${{ secrets.KUBERNETES_CLUSTER_CONFIG }}
.
For å få på plass en workflow som både bruker Docker og deployer til Kubernetes, kommer vi til å bruke flere ferdiglagde actions:
- docker/login-action, som logger
docker
inn på container-registeretdevops101registry.azurecr.io
. - docker/build-push-action, som bygger og pusher Docker images, og som er avhengig av at action docker/setup-buildx-action er kjørt først.
- azure/setup-kubectl som installerer
kubectl
. - azure/k8s-set-context som konfigurerer
kubectl
til å bruke kubeconfig-filen vi har lagt inn i Repository secrets. - azure/k8s-deploy som deployer oppdatert Kubernetes-konfigurasjon til
devops-101-cluster
.
Under er en mangelfull workflow du kan ta utgangspunkt i, ved å legge den inn i en fil som f.eks. heter hello-deploy.yml
under mappen .github/workflows
.
name: "Hello Deploy"
on:
workflow_dispatch:
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: "Login to container registry"
uses: docker/login-action@v2
with:
registry: devops101registry.azurecr.io
username: [HVA MANGLER HER?]
password: [HVA MANGLER HER?]
- name: "Set up Docker Buildx"
uses: docker/setup-buildx-action@v2
- name: Build and push
uses: docker/build-push-action@v3
with:
[HER MANGLER FLERE LINJER]
deploy:
runs-on: ubuntu-latest
needs: build
steps:
- uses: actions/checkout@v3
- uses: azure/setup-kubectl@v3
- uses: azure/k8s-set-context@v1
with:
method: kubeconfig
kubeconfig: [HVA MANGLER HER?]
context: devops-101-cluster
- uses: Azure/[email protected]
with:
[HER MANGLER FLERE LINJER]
Oppgave: Fyll inn det som mangler i workflowen over, sånn at du kan kjøre den fra GitHub og deploye nye versjoner av Notes.Api
. Hvis du står fast, er det bare å spørre om hjelp, eller ta en titt på forslaget til løsning her.
Tips: For å lage en image-tag som er unik for hver kjøring av workflowen, kan det være nyttig å bruke miljøvariabelen github.run_number
sånn at image-taggen man bruker i workflowen er noe i retning av devops101registry.azurecr.io/notes-api:v${{ github.run_number }}
.
Nå kan vi knytte sammen to av workflowene vi har skrevet til en litt større CI/CD-pipeline. Med litt flaks har vi en hello-dotnet.yml
-workflow som kjører tester for å sjekke om applikasjonen ser ut til å fungere som forventet. I tillegg har vi en hello-deploy.yml
-workflow som kan deploye applikasjonen.
Med dette i bahodet kan vi vurdere hvordan disse to workflowene kunne ha vært organisert i en CI/CD-pipeline:
- Når en utvikler har skrevet ny kode, hadde det vært nyttig å kreve at man må gjennom en pull request før man får merge til
main
-branchen i repoet. I tillegg hadde det vært nyttig å kreve at testene ihello-dotnet.yml
kjører ok. Dette kunne man fått til ved å trigge denne workflowen når man har en pull request, og sette opp en branch protction rule. - Videre hadde det vært nyttig å deploye all kode etter den er merget inn til
main
-branchen. Dette kunne man fått til ved å sette opp en trigger som kjørerhello-deploy.yml
-workflowen når det kommer inn en ny commit påmain
-branchen.
Oppgave: Se om du kan sette opp noen triggere på hello-dotnet.yml
og hello-deploy.yml
-workflowene, som setter de sammen i en litt større CI/CD-pipeline. Gi gjerne workflowene nye navn som passer bedre med rollen de har i pipelinen.
Spørsmål: Hvilke feil fanges opp av den CI/CD-pipelinen du nå har satt opp? Hvilke feil fanges ikke opp?