A labor célja, hogy egy összetettebb cloud-native webalkalmazást készítsünk az Azure Platform-as-a-Service (PaaS) és egyéb szolgáltatásait felhasználva.
A labor alapjául Szabó Márk Tech Summit Budapest 2019-es előadása szolgált, amely átdolgozásra került erre a tárgyra.
Tartalomjegyzék
Feladatunk a következők:
- Funkcionális követelmények:
- Készítsünk webalkalmazást, ahova kutyusokat és cicákat lehet feltölteni, hogy új gazdira találhassanak.
- A user tudja böngészni az állapotokat
- A user tud feltölteni új képet a gazdát kereső állatról
- Az alkalmazás döntse el automatikusan, hogy a képen milyen állat szerepel (kutya, vagy macska), és ez alapján adjon egy előzetes kategória javaslatot.
- A képről vágja le a nem releváns részeket
- Technológiai követelmények:
- Perzisztáljuk az állatok adatait és a képeket
- Optimalizáljunk a statikus fájlok kiszolgálását
- Legyen a kitelepített alkalmazás működése jól nyomonkövethető, debuggolható
- A fejlesztőkkel ne osszuk meg az érzékeny szolgáltatás kulcsokat
A megvalósításunk legyen a következő:
- A user egy ASP․NET Core-os REST API backendet és egy React-os frontend alkalmazást fog használni
- Ez egy Azure App Service-be legyen kitelepítve
- A backend az adatok perzisztens tárolására használjon Cosmos DB NoSQL adatbázist
- A feltöltött képeket a backend egy Blob Storage-ba mentse el
- A képek klasszifikációjára használjunk az Azure Cognitive Services családból a Custom Vision gépi tanulás megoldását
- Ezt közvetlenül a backendünk fogja hívni egy REST API-n keresztül
- A képek kivágását végezzük aszinkron módon
- Az kivágandó képeket rakjuk be egy feldolgozási sorba
- Ehhez most Queue Storage-et fogunk használni (alternatíva lehetne még az Azure Service Bus)
- A sorban lévő feladatokat egy serverless Azure Function fogja feldolgozni
- A feladatokhoz tartozó adatokat az adatbázisból, a képeket a blob tárhelyről veszi
- A képek kivágását szintén egy Cognitive Service fogja végezni
- Az állatok nem jelennek meg addig a felületen, amíg ez a háttérművelet be nem fejeződött. Ezt egy flaggel jelezzük a DB-ben. Ha végzett a feladatával a function, akkor publikáltra állítja az állat rekordját és frissíti azt az új képpel.
- Az kivágandó képeket rakjuk be egy feldolgozási sorba
- A statikus fájlok kiszolgálásának optimalizációjára használjuk az Azure CDN szolgáltatását
- Esetünkben most az állatok képeit tároljuk itt
- A kitelepített környezet szolgáltatásainak kulcsait tároljuk Azure Key Vault-ban
- A kitelepített alkalmazás monitorozásához integráljuk az Application Insights szolgáltatást
🛠 Klónozzuk le a kiinduló projektet a C:\work\[neptun]\ mappánkon belül egy új mappába.
mkdir c:\work\[neptun]\complex-paas
cd c:\work\[neptun]\complex-paas
git clone https://github.com/bmeaut/azure-complex-paas-labor.git
🛠 Nyissuk meg a MyNewHome.sln solution-t és tekintsük át azt.
TODO
Sok minden előre elkészítve van már nekünk. Most nem kódolni szeretnénk, hanem összerakni azt a felhő architektúrát, amit az előző fejezetben megálmodtunk. A további implementáció a laborútmutatóból másolhatók, egy egy kevés magyarázat is tartozik hozzájuk.
🛠 Hozzunk létre az azure portálon egy új Resource Group-ot MyNewHome
néven. Ebbe fogunk a mai órán dolgozni.
🛠 Hozzunk létre egy új Web App-ot a MyNewHome
resource groupba mynewhome-[neptun]
néven. Ilyenkor a mynewhome-[neptun].azurewebsites.net
címen lesz majd elérhető a webalkalmazásunk.
Beállítások
- Publish: code
- Runtime .NET Core 2.2
- OS: Windows
- Region: West EU
- App Service Plan:
- Hozzunk létre egy új plant a Create New gombbal
MyNewHomePlan
néven - Az ingyenes F1 csomag elég lesz most nekünk
- Hozzunk létre egy új plant a Create New gombbal
- Monitoring fülön kapcsoljuk be az App Insights-ot, egy új példány létrehozásával (default)
A kiinduló projektet publikáljuk ki az App Service-be. Ezt otthon legegyszerűbben úgy tudjuk megtenni, hogy a Visual Studioba bejelentkezünk a fiókunkkal, ami után a webes projekten jobb gomb / Publish varázslóval könnyedén tudunk deployolni. Mivel labor gépen nem szeretnénk bejelentkezni, használjuk inkább az előre elkészített konfigurációs állományt (publish profile), ami lényegében egy XML fájl.
🛠 Töltsük le a Get publish profile gombbal az állományt
🛠 Publikáljuk ki a projektet a VS-ből:
- projekten jobb gomb / Publish
- import profile, majd tallózzuk ki a letöltött configot
- Publish indítása
ASP․NET Core esetben a konfigurációt az alkalmazás több helyről olvassa fel: konzol argumentumok, környezeti változók, application.json, (lokális debug esetben client secrets). Mi ezt szeretnénk most kiegészíteni azzal, hogy az Azure Key Vault-ból is olvassa fel a konfigurációt, ha élesbe telepítettük ki az alkalmazásunkat.
🛠 Hozzunk létre egy új Azure Key Vault-ot az aktuális resource groupunkba MyNewHome-[neptun]-KeyVault
néven
- Region: West EU
- Pricing Tier: Standard
A Key Vaulthoz minden hozzáférés alapvetően le van tiltva. Most olyan authentikációs módszert választunk, ahol a web alkalmazást futtató service user (system assigned managed identity) nevében fogunk hozzáférni a biztonságos tárhoz.
🛠 Kapcsoljuk be az App Service / Identity menüben a system assigned managed identity beállítást
Ilyenkor létrejön egy user, akinek a nevében fog futni az App Service-ünk. Erre azért lesz szükség, hogy be tudjuk állítani a Key Vaultban a hozzáférési jogosultságokat.
🛠 Állítsuk be a jogosultságokat a Key Vault-ban
- Key Vault / Access policies / Add Access Policy
- Configure from template: Key and Secret management
- Key permissions: nekünk elég most csak a Get és a List
- Secret permissions: nekünk elég most csak a Get és a List
- Select principal: újonnan létrehozott managed identity (tipikusan az app service neve)
- Add gomb
🛠 Vegyük fel az Azure Key Vault-hoz kapcsolódó NuGet csomagokat a MyNewHome.Infrastructure
projektbe.
<PackageReference Include="Microsoft.Extensions.Configuration.Binder" Version="2.2.4" />
<PackageReference Include="Microsoft.Extensions.Configuration.AzureKeyVault" Version="2.2.0" />
🛠 Valósítsuk meg a MyNewHome.Infrastructure
projektben lévő ConfigurationBuilderExtensions.AddAzureKeyVault()
segédfüggvényt.
public static IConfigurationBuilder AddAzureKeyVault(this IConfigurationBuilder builder)
{
var config = builder.Build();
var keyVaultBaseUrl = config.GetValue<string>("KeyVault");
var azureServiceTokenProvider = new AzureServiceTokenProvider();
var keyVaultClient = new KeyVaultClient(
new KeyVaultClient.AuthenticationCallback(
azureServiceTokenProvider.KeyVaultTokenCallback));
builder.AddAzureKeyVault(keyVaultBaseUrl, keyVaultClient, new DefaultKeyVaultSecretManager());
return builder;
}
Az az oka annak, hogy külön projektben van ez a konfiguráció, hogy majd az Azure Function projektünk is tudja használni ezt a kódot.
Figyeljük meg, hogy az aktuális configból olvassuk ki az URL-t, KeyVault
kulcsú beállításként. Ezt most környezeti változóként fogjuk kezelni a telepített alkalmazásban. A managed identity authentikációt a KeyVaultClient
megoldja, ha a fenti beállításokat választjuk.
🛠 Adjuk meg a Web Appban, a használandó Key Vault URL-jét, amit a Key Vault áttekintő nézetéről tudunk kimásolni. Megadni az App Service / Configuration / Application Settings / New application setting opcióval tudjuk. Kulcs: KeyVault
, érték: a kimásolt Key Vault URL.
🛠 Az API projekt Program
osztályában használjuk az AddAzureKeyVault
segédfüggvényünket, de csak akkor, ha éles környezetben vagyunk.
public static IWebHostBuilder CreateWebHostBuilder(string[] args) =>
WebHost.CreateDefaultBuilder(args)
.ConfigureAppConfiguration((context, builder) =>
{
if (context.HostingEnvironment.IsProduction())
{
builder.AddAzureKeyVault();
}
})
.UseStartup<Startup>();
🛠 Indítsuk újra a Web Appot és próbáljuk ki.
A Key Vault-unkban még nincs semmi, de nem is használja most az alkalmazás semmire.
Megj.: Most az
IConfiguration
-t közvetlenül használjuk mindenhol. Egy éles alkalmazásban érdemes lenne használni az Options mintát (IOption<T>
), hogy erősen típusosan kezeljük a konfigurációinkat. https://docs.microsoft.com/en-us/aspnet/core/fundamentals/configuration/options?view=aspnetcore-3.0
Az alkalmazásunk adatait egy Cosmos DB fogja tárolni. Most csak egy entitásunk lesz a Pet
, így elég a legegyszerűbb konfiguráció. A képeket pedig kanonikus módon egy Blob Storage-ba fogjuk tenni.
🛠 Hozzunk létre a recource groupunkba egy Cosmos DB példányt
- Account name:
mynewhome-[neptun]-db
- API: Core
- Apache Spark: None
- Location: West EU
- Geo redundancy: Disable
- Multi-region writes: Disable
Amíg ez teker térjünk át a Storage-ra.
🛠 Hozzunk létre egy Storage Accountot a resource groupunkba mynewhome[neptun]storage
néven.
🛠 A Key Vault-ban adjuk meg a Cosmos DB és a Storage connection string-jeit Secret-ként az alábbi kulccsal és értékekkel
CosmosConnectionString
: Cosmos DB / Keys / PRIMARY CONNECTION STRINGStorageConnectionString
: Storage / Keys / Connection String
🛠 Tekintsük át hogyan használjuk a Cosmos DB-t a PetService
-ben. Lényegében a CRUD műveleteket valósítottuk meg most az alacsony szintű API-n keresztül. Minimális ORM funkcionalitást kapunk, mert a Pet
osztályt tudjuk használni a műveletek során, de például a lekérdezéseket már nem tudjuk LINQ-kel megvalósítani. Ha itt is ORM-et szeretnénk használni akkor érdemes megvizsgálni az Entity Framework Core 3.0 Cosmos DB támogatását.
🛠 Implementáljuk a PetController
UploadAndRecognizeImage
metódusában a Blob storage kezelését.
[HttpPost("upload")]
public async Task<ActionResult> UploadAndRecognizeImage()
{
var image = Request?.Form?.Files?[0];
if (image == null) return BadRequest();
// Retrieve a reference to a container
var container = _storage.CreateCloudBlobClient().GetContainerReference("pets");
// Create the container if it doesn't already exist
await container.CreateIfNotExistsAsync();
// Set container access level
await container.SetPermissionsAsync(new BlobContainerPermissions { PublicAccess = BlobContainerPublicAccessType.Container });
string ext = GetImageExtension(image.ContentType);
if (ext == null) return BadRequest();
// Upload image from stream with a generated filename
var blob = container.GetBlockBlobReference(Guid.NewGuid().ToString() + "." + ext);
await blob.UploadFromStreamAsync(image.OpenReadStream());
var url = blob.Uri.AbsoluteUri;
// TODO recognize pet type
return Ok(new { url, type = "", probability = 0 });
}
private readonly PetService _petService;
private readonly CloudStorageAccount _storage;
public PetController(PetService petService, IConfiguration configuration)
{
_petService = petService;
_storage = CloudStorageAccount.Parse(configuration["StorageConnectionString"]);
}
private string GetImageExtension(string contentType)
{
switch (contentType)
{
case "image/png": return "png";
case "image/jpeg": return "jpeg";
case "image/jpg": return "jpg";
case "image/gif": return "gif";
case "image/bmp": return "bmp";
case "image/ief": return "ief";
case "image/svg+xml": return "svg+xml";
case "image/raw": return "raw";
default: return null;
}
}
}
A kód lényegében létrehoz egy klienst, amin keresztül létrehozunk egy konténert pets
néven, publikus hozzáféréssel, majd ebbe a konténerbe feltöltjük a képet. A kliensnek leküldjük ezt az URL-t, hogy meg tudja jeleníteni a felületen. A type
és a probability
mezőket most csak mock értékekkel feltöltjük. Ezeket fogja majd a kognitív szolgáltatásunk tölteni.
Megj.: Most nem töltjük az időt, hogy szépen kiszervezzük ezt a kódot. Egy éles alkalmazásban érdemes lenne ezeket külön service osztályokba szervezni.
🛠 Indítsuk újra a web appot! Próbáljuk ki!
- Töltsünk fel egy új kutyust/cicát.
- Nézzük meg, hogy a storage-ben megjelent-e a képe
- Storage / Storage Explorer / Blobs
- Nézzük meg, hogy a DB-be is bekerültek-e az adatok.
- Cosmos DB / Data Explorer / pets / items
- Írjuk át a published tulajdonságot
true
-ra: megjelenik a felületen a kutyus.
Az állatok klasszifikációjához és a kép kivágásához az Azure Cognitive Services szolgáltatásait fogjuk igénybe venni, amik mesterséges intelligencia alapú megoldásokat nyújt sok problémára, nagyon egyszerű módon. A klasszifikációhoz a Custom Vision komponenst fogjuk feltanítani egy betanító adathalmazzal, ami alapján majd becslést tud adni az újonnan kapott képeken látható állat fajáról.
🛠 Hozzunk létre egy új Custom Vision erőforrást a resource groupunkba MyNewHome-CustomVision
néven.
- Training, Prediction Location: West EU
- Training, Prediction Pricing Tier: F0
Ez még csak az Azure-os erőforrás, ami esetünkben csak a számítási kapacitást és a számlázási egységet adja. Ebben még külön projekteket kell definiáljunk, ahol feltaníthatjuk a mesterséges intelligenciát.
🛠 Hozzunk létre egy új projektet és tanítsuk fel néhány tesztadattal a modellt
- Nyissuk meg a https://www.customvision.ai/projects oldalt
- Ügyeljünk, hogy a jobb felső sarokban jó subscription legyen kiválasztva
- Hozzunk létre egy új projektet
- Name:
CatOrDog
- Resource:
MyNewHome-CustomVision
- Project Type: Classification => csak címkézni akarjuk a képeket tartalmuk alapján
- Classification Types: Multiclass => Egy képhez egy címket (tag) tartozhat
- Domain: General
- Name:
- A projektbe töltsük fel a macskás képeinket a kiinduló projekt
test-images/cats
mappájából és adjunk nekicat
tag-et, majd ismételjük meg ezt a kutyákkal is atest-images/dogs
mappábóldog
taggel - Kattintsunk a Train gombra, és válasszuk a Quick opciót
- A Quick test gombra kattintva próbáljuk ki a feltanított modellt egy internetről kitallózott képpel
- Figyeljük meg, hogy a Quick test eredményei megjelennek a Predictions fül alatt is, ahol ezekre is megadhatjuk a címkéket, amivel tovább taníthatjuk a modellt a Train gomb megnyomásával
- A használni kívánt iterációt publikáljuk a Performance fül alatt
🛠 Hívjuk meg a feltanított Custom Vision API-nkat a PetController
-ben.
private readonly CustomVisionPredictionClient _customVision;
private readonly Guid _customVisionId;
public PetController(PetService petService, IConfiguration configuration, IHttpClientFactory httpClientFactory)
{
_petService = petService;
_storage = CloudStorageAccount.Parse(configuration["StorageConnectionString"]);
_customVision = new CustomVisionPredictionClient(httpClientFactory.CreateClient(), false)
{
ApiKey = configuration["CustomVision:ApiKey"],
Endpoint = configuration["CustomVision:Url"],
};
_customVisionId = new Guid(configuration["CustomVision:ProjectId"]);
}
var prediction = await _customVision.ClassifyImageUrlAsync(_customVisionId, "Iteration2", new ImageUrl(url)); // Figyeljünk oda az iteráció nevére
var tag = prediction.Predictions.OrderByDescending(p => p.Probability).First();
🛠 Vegyük fel a Key Vaultba a Custom Vision-höz tartozó secreteket:
CustomVision--ApiKey
kulccsal az Azure portálon Custom Vision / Quick start / Api key1 értékét.CustomVision--Url
kulccsal az Azure portálon Custom Vision / Quick start / Url értékét.- Vigyázzunk mert van, hogy egy teljesen új erőforrást hoz létre a prediction-nek a custom vision. Ennek a kulcsát és URL-jét használjuk!
CustomVision--ProjectId
kulccsal a custom vision portálon a projekt guidját, amit az url-ben találunk
Megj.: Figyeljük meg hogy a hierarchikus config kulcsokat az Azure Key Vaultban
:
helyett--
karakterekkel kell elválasztani.
🛠 Publikáljuk a webes projektünket és próbáljuk ki a feltöltést. Fel kell ismernie, az állat típusát a képről.
A kép okos kivágására az Azure Computer Vision szolgáltatását fogjuk használni. Maga a feldolgozás a tervezett architektúránknak megfelelően aszinkron történik. A feldolgozandó elem adatait egy Queue Storage-ba fogjuk belerakni. Ezt az üzenetsort egy serverless komponens (Azure Function) fogja figyelni, és aktiválódik, ha van új feladat, majd elvégzi a feldolgozást. Számunkra azért is előnyös lehet a serverless megoldás, mivel lehet hívás alapon számlázni, és szinte a végtelenségig skálázható akár function-önkét.
🛠 Hozzunk létre az Azure portálon egy Computer Vision erőforrást MyNewHome-ComputerVision
néven.
Ezt szintén egy REST API-n keresztül fogjuk majd elérni, további konfigurációt nem igényel, mivel ez egy SaaS, és az előre elkészített funkcióit fogjuk használni.
🛠 Rakjunk az üzenetsorba egy üzenetet a PetController
PostPet
metódusában.
[HttpPost]
public async Task<ActionResult<Pet>> PostPet([FromBody] Pet pet)
{
pet = await _petService.AddPetAsync(pet);
// Retrieve a reference to a queue
var queue = _storage.CreateCloudQueueClient().GetQueueReference("newpets");
// Create the queue if it doesn't already exist
await queue.CreateIfNotExistsAsync();
// Create a message and add it to the queue
var message = new CloudQueueMessage(pet.ToString());
await queue.AddMessageAsync(message);
return CreatedAtAction(nameof(GetPetsAsync), new { id = pet.Id }, pet);
}
Most az egyszerűség kedvéért használtunk Queue storage-et. Egy összetettebb alkalmazás esetében (pl.: Microservice architektúra) érdemes megfontolni egy robosztusabb Queue szolgáltatás használatát. Erre példa az Azure Service Bus.
🛠 Publikáljuk az alkalmazást
A projektben már elő van készítve egy Azure Functions projekt MyNewHome.Functions
néven. Ha megvizsgáljuk láthatjuk, hogy maga a function egy statikus Run
metódusból áll, aminek az aktiválásának módját a QueueTrigger
attribútum adja meg. Ha megjelenik egy új elem a queue-ban akkor meghívódik a function. Az azure key vault és a dependency injection használatához kicsit maszírozni kellett a function projektet, de ez előkészítve működik most nektek. TODO még egy kis magyarázat
🛠 Hozzuk létre az Azure portálon egy Function appot MyNewHome-i6rxee-functions
néven és konfiguráljuk fel.
- Runtime Stack: .NET Core
- Region: West EU
- Hosting
- Válasszuk ki a storage accountunkat
- Plan type: Consumption
- Ilyenkor a meghívások száma után fizetünk és ilyenkor skálázódik magától a végtelenségig. Lehetőségünk lenne még egy meglévő App Service Plan-be telepíteni az appunkat, olyankor a skálázás az adott App Service Plan feladata.
- Create
- A function app-ban adjuk meg a Configuration menüben az Azure Key Vault-unk elérési útvonalát az App Service mintájára
- A Platform beállításokban kapcsoljuk be a System Managed Identity-t, majd adjuk hozzá az Azure Key Vault-ban az Access Policy-khez az App Service mintájára.
🛠 Cseréljük le a Function tetején lévő URL-t a saját Computer Vision URL-ünkre.
🛠 Publikáljuk a Functions appot az exportált publish profile állománnyal.
🛠 Próbáljuk ki! Töltsünk fel egy új képet, és várjunk amíg meg nem jelenik a felületen a feldolgozott rekord.
A CDN-nel lehetőségünk van optimalizálni a statikus fájlok elérését, mégpedig úgy, hogy a felhasználóhoz közeli adatközpontban elcache-eljük azt. Most a blob storage-ban lévő állatok képére készítsünk ilyen cachet.
🛠 Ellenőrizzük, hogy az Azure fiókunkban engedélyezve van-e a CDN szolgáltatás használata, ha nem engedélyezzük: Subscriptions / [előfizetésünk] / Resource providers / Microsoft.Cdn
🛠 Hozzunk létre egy CND erőforrást MyNewHome-CDN
néven:
- Pricing: Standard Microsoft
- Create new CDN Endpoint
- url:
mynewhome-i6rxee-storage-cdn
- origin type: storage
- origin hostname: storage accountunk
- url:
🛠 Vegyük fel az Azure Key Vault-ba a CDN elérési útját ImageCdnHost
kulccsal.
🛠 Írjuk felül a CDN elérési útjával a Cosmos DB-ben az állat képének URL-jét.
// Swap url host to CDN
var url = new Uri(new Uri(config.GetValue<string>("ImageCdnHost")), blob.Uri.PathAndQuery).AbsoluteUri;
// publish pet
var pet = await petService.GetPetAsync(petFromQueue.Id, petFromQueue.Type);
pet.ImageUrl = url;
pet.Published = true;
await petService.UpdatePetAsync(pet);
🛠 Publikáljuk a Functions appot és próbáljuk ki! F12-vel már azt kell látnunk, hogy az újonnan feltöltött képek esetében a CDN-ről jönnek le a képek és nem a Blob-ból közvetlenül.
TODO snapshot debugging, publish profile
try
{
var prediction = await _customVision.ClassifyImageUrlAsync(_customVisionId, "Iteration2", new ImageUrl(url));
var tag = prediction.Predictions.OrderByDescending(p => p.Probability).First();
return Ok(new { url, type = tag.TagName, probability = tag.Probability });
}
catch (Exception ex)
{
_telemetryClient.TrackException(ex);
throw;
}
TODO
_telemetryClient.TrackEvent(
"New pet added.",
new Dictionary<string, string>
{
{ "Pet type", pet.Type.ToString() },
},
new Dictionary<string, double>
{
{ "New pet", 1 },
});