ASP.NET Core + Docker + Azure Container Instances

Häromdagen blev jag inspierad av ett avsnitt av Azure Friday, https://azure.microsoft.com/sv-se/resources/videos/azure-friday-code-to-cloud-with-docker-and-azure-container-instances/ och idén till den här posten föddes.

I den här laborationen ska vi:

  • Bygga (kompilera) en ASP.NET Core Web Application, ett web api, i en docker container
  • Köra webbapplikationen i "samma container"
  • Köra containern lokalt på sin utvecklardator
  • Köra containern i molnet, i tjänsten Azure Container Instances (ACI)

Sist presenteras också ett litet exempel med en färdig image (nginx) och docker compose.

Jag tänkte använda följande verktyg:

Labben kommer att genomföras i en katalog .../acilab/ och webbapplikationen skapas i acilab/a-web-api


Notera att alla kommandon körs i Powershell, allt från att ratta Docker till att skapa resurser i Azure. Resurserna syns såklart i https://portal.azure.com och det kan vara nyttigt att titta där när man tagit sig igenom första stegen och ska göra saker i molnet.

Vi drar igång!

Skapa webbprojektet

Jag tänkte skapa ett ASP.NET Core Web API och detta görs enkelt genom följande kommando i katalogen acilab:

acilab> dotnet new webapi -o a-web-api  

Om man kör igång det alldeles nyskapade webbprojektet genom följande kommando:

acilab> dotnet run -p .\a-web-api\a-web-api.csproj  

så kan man surfa till https://localhost:5001/api/weatherforecast med sin favoritwebbläsare eller kan man göra webbanropet i Powershell genom:

[godtycklig katalog]> Invoke-WebRequest https://localhost:5001/weatherforecast

StatusCode        : 200  
StatusDescription : OK  
Content           : [{"date": ...  
...

OK! Webbapplikationen är skapad och den fungerar.

Bygga projektet mha en container

Det här steget kan tyckas en smula onödigt eftersom webbprojektet redan är byggt och testat ovan. Men det fina med att låta en container bygga projektet är att man blir helt oberoende av sin omgivning för att bygga källkoden.

Vi gör detta genom att, i katalogen acilab, skapa en fil, Dockerfile, enligt nedan:

##
##### Create the sdk-env(ironment) and publish the project
FROM mcr.microsoft.com/dotnet/core/sdk:3.1 AS sdk-env  
WORKDIR /webapp

# Copy cs-proj and restore nugets
COPY a-web-api/*.csproj ./  
RUN dotnet restore

# Copy source and build/publish
COPY a-web-api/. ./  
RUN dotnet publish a-web-api.csproj -o publish-app

##
##### Create the runtime environment
FROM mcr.microsoft.com/dotnet/core/aspnet:3.1  
WORKDIR /webapp

# Copy the published web application
COPY --from=sdk-env /webapp/publish-app .

# Run it!
ENTRYPOINT ["dotnet", "a-web-api.dll"]  

Dockerfile är i det här exemplet tvådelad:

  • skapa en sdk-omgivning för att bygga och publicera webbapplikationen
  • kör webbapplikationen

Om man är bekväm med .NET Core CLI och till viss del även uppbyggnaden av Dockerfile så är ovan ganska rakt på.

För att skapa docker-imagen, gör följande:

acilab> docker build -t a-web-api-c .  

Detta kan ta en och annan minut beroende på hur mycket som måste laddas ned av dom två bas-images som den önskade docker-imagen innehåller.

Output från bygget bör se ut något i stil med:

Sending build context to Docker daemon  733.2kB  
Step 1/10 : FROM mcr.microsoft.com/dotnet/core/sdk:3.1 AS sdk-env  
 ---> c4155a9104a8
Step 2/10 : WORKDIR /webapp  
 ---> Using cache
 ---> 3ba2524dad80
Step 3/10 : COPY a-web-api/*.csproj ./  
 ---> Using cache
 ---> 515945cd7bfc...
...

Kör webbapplikationen/containern

Nu är container-imagen klar och redo att instansieras att köras igång. Detta görs genom:

acilab> docker run -p 8080:80 a-web-api-c  

Testa containern genom:

[godtycklig katalog]> Invoke-WebRequest http://localhost:8080/weatherforecast

Output borde bli väldigt snarlikt output när motsvarande anrop gjordes tidigare, den gången mot https://...:5001.

Köra containern i ACI

Nu börjar det riktigt roliga!

  • Logga in och koppla upp Docker CLI mot Azure Container Instances, genom följande och ange inloggningsuppgifter:
acilab> docker login azure  
  • Skapa en context för Docker CLI att jobb i och växla till den. När contexten skapas kommer man eventuellt välja subscription och om man vill skapa en ny resursgrupp för containern i Azure:
acilab> docker context create aci an-aci-context

? Select a subscription ID Visual Studio Professional (...)
? Select a resource group create a new resource group
Resource group "57adb381-634b-40c6-99e2-6d8fcfe137b8" (eastus) created  
Successfully created aci context "an-aci-context"

acilab> docker context use an-aci-context  

Notera namnet på resursgruppen, här "57adb381-634b-40c6-99e2-6d8fcfe137b8". Detta kommer att bli något annat varje gång en context skapas och man väljer att skapa en ny resursgrupp.

Om man nu kontrollerar vilken context docker CLI befinner sig i, genom följande kommando, så borde output se ut ungefär så här:

acilab> docker context ls  
NAME                TYPE                DESCRIPTION ...  
an-aci-context *    aci                 57adb381-634b-40c6-99e2-6d8fcfe137b8@eastus ...  
default             moby                Current DOCKER_HOST based configuration ...  
...

Detta betyder att man är i an-aci-context, typen är ACI (Azure Container Instances) och description pekar på den nyskapade resursgruppen. Det är i den gruppen som kommande containrar kommer att hamna.

Notera dock att man befinner sig, i kommandotolken/Powershell, i det lokala filsystemet:

acilab> ls

    Directory: .......\acilab

Mode                 LastWriteTime         Length Name  
----                 -------------         ------ ----
d-----         9/23/2020   6:49 AM                a-web-api  
-a----         9/23/2020   7:35 AM            587 Dockerfile

Man har alltså tillgång till källkoden för sin webbapplikation och Dockerfile. Detta borde göra det möjligt att bygga och köra sin container, i ACI. Vi provar!

Första försöket, rakt på => fail!

Vi fortsätter precis där vi slutade ovan och testar följande:

acilab> docker build -t a-web-api-c .  

Tyvärr falerar det här kommandot på grund av att det inte är möjligt att bygga sin image i den här contexten. Man måste istället få upp sin image till ett container registry och ett sådant skapar med relativt enkelt, i samma resursgrupp.

Andra försöket, nu via ett container registry => success?

Enligt ovan så måste vi alltså skapa ett registry och låta imagen bo där. Vi skapar ett registry i Azure, i samma resursgrupp som ACI-contexten skapade:

acilab> az login  
[{...}]
acilab> az acr create --resource-group 57adb381-634b-40c6-99e2-6d8fcfe137b8 --name acilab --sku Basic  
{...}

Varje kommando returnera ett json-objekt med resultatet av operationen.

Namnet på registryt blir acilab.azurecr.io och kommer att spilla ner på taggningen av imagen.

Nu har vi ett registry att pusha den lokala imagen till. Detta görs enkelt genom följande:

acilab> docker context use default  
acilab> docker tag a-web-api-c acilab.azurecr.io/a-web-api-c  
acilab> docker push acilab.azurecr.io/a-web-api-c  

Om man nu växlar till ACI-contexten som man skapade tidigare så borde man kunna skapa och köra sin container. Gör detta genom följande:

acilab> docker context use an-aci-context  
acilab> docker run -d -p 80:80 acilab.azurecr.io/a-web-api-c  

När containern startat, testa den genom att göra webbanropet på samma sätt som tidigare. Aktuell ip-adress för containern i ACI får man enkelt reda på genom att göra:

acilab> docker ps  
CONTAINER ID            IMAGE ...                         ... PORTS  
compassionate-satoshi   acilab.azurecr.io/a-web-api-c ... ... 52.226.143.206:80->80/tcp  

Testanropet med tillhörande resultat ser då ut enligt:

[godtycklig katalog]> Invoke-WebRequest http://52.226.143.206/weatherforecast

StatusCode        : 200  
StatusDescription : OK  
Content           : [{...  
...

Succé! Containern kör i Azure och är åtkomlig publikt på internet.

Notera att exemplet visar ett concept, väldigt hands-on och ad-hoc och är INTE avsett att användas i produktion.

Städa upp i Azure efter laborationen

Vill man städa bort skapade resurser i Azure så görs det enkelt genom följande kommandon:

acilab> docker stop compassionate-satoshi  
acilab> docker rm compassionate-satoshi  
acilab> docker context use default  
acilab> docker context rm an-aci-context  
acilab> Remove-AzResourceGroup -Name "57adb381-634b-40c6-99e2-6d8fcfe137b8"  

Det sista kommandot tar en stund att köra då den ska städa upp både ACI och det skapade registryt.

Notera resursgruppnamnet, som ska vara samma som det som returnerades när docker CLI-contexten skapades i ACI ovan.

Men färdiga images då?

Om man vill köra färdiga images i ACI så går det såklart. Detta görs enklast genom att man använder docker compose. Man behöver inte ha en egen Dockerfile, inget container registry eller något sådant. Låt oss titta på ett exempel!

nginx-image "Welcome..."

För att göra det enkelt för oss så väljer vi default-imagen för nginx. Kör man igång den och surfar till sajten så visas "Welcome to nginx!"-sidan. Imagen är liten och går snabbt att skapa.

Låt oss köra lokalt först och sen i ACI.

Lokalt

I en katalog (az-nginx), lokalt på din maskin, skapa en docker-compose.yml-fil:

version: '3.7'  
services:  
    lab-web:
      image: nginx
      ports:
       - "80:80"

Start och testa den genom:

az-nginx> docker-compose up -d  
Starting az-nginx_lab-web_1 ... done  
az-nginx> Invoke-WebRequest http://localhost

StatusCode        : 200  
StatusDescription : OK  
Content           : <!DOCTYPE html>...  
...

Stäng av containern genom:

az-nginx> docker-compose down  
Stopping az-nginx_lab-web_1 ... done  
Removing az-nginx_lab-web_1 ... done  
Removing network az-nginx_default  

I ACI

Nu är det dags att skapa en docker-context i ACI och start containern där:

az-nginx> docker context create aci an-aci-context

? Select a subscription ID Visual Studio Professional (...)
? Select a resource group create a new resource group
Resource group "63e26706-9e84-452a-84c7-7fa0e5bca998" (eastus) created  
Successfully created aci context "an-aci-context"

az-nginx> docker context use an-aci-context  
an-aci-context

az-nginx> docker compose up  
[+] Running 2/2
 - Group aznginx  Created                                                  7.1s
 - lab-web        Done                                                    21.4s

az-nginx> docker ps  
CONTAINER ID        IMAGE ...    STATUS         PORTS  
aznginx_lab-web     nginx ...    Running        52.191.90.126:80->80/tcp

az-nginx> Invoke-WebRequest http://52.191.90.126  
StatusCode        : 200  
StatusDescription : OK  
Content           : <!DOCTYPE html>...  

Notera den lilla skillnaden när compose-filen ska köras. Man använder här istället "docker compose up" UTAN bindestreck. Docker compose är i ACI-versionen av docker CLI en del av den vanliga CLI:n.

Stäng av containern på samma sätt som tidigare:

az-nginx> docker compose down  

Städa upp igen

Städningen görs på samma sätt som ovan, men i det här exemplet finns det inget registry att plocka bort:

az-nginx> docker context use default  
az-nginx> docker context rm an-aci-context  
az-nginx> Remove-AzResourceGroup -Name "63e26706-9e84-452a-84c7-7fa0e5bca998"  

På samma sätt som ovan så är resursgruppnamnet det som skapades när man valde att ska en ny grupp i samband med att man skapade en docker-cli-context i ACI.