cloud

Összetettebb Azure PaaS alkalmazás

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 - [Összetettebb Azure PaaS alkalmazás](#%c3%96sszetettebb-azure-paas-alkalmaz%c3%a1s) - [Feladat](#feladat) - [Architektúra](#architekt%c3%bara) - [Megvalósítás](#megval%c3%b3s%c3%adt%c3%a1s) - [Kiinduló projekt](#kiindul%c3%b3-projekt) - [App Service](#app-service) - [Key Vault](#key-vault) - [Cosmos DB és Storage](#cosmos-db-%c3%a9s-storage) - [Klasszifikáció - Cognitive Service Custom Vision](#klasszifik%c3%a1ci%c3%b3---cognitive-service-custom-vision) - [Kép kivágása](#k%c3%a9p-kiv%c3%a1g%c3%a1sa) - [Custom Vision](#custom-vision) - [Queue storage](#queue-storage) - [Azure Function](#azure-function) - [Azure CDN](#azure-cdn) - [Application Insights](#application-insights) - [Track Exception](#track-exception) - [Track Event](#track-event)

Feladat

Feladatunk a következők:

Screenshot

Architektúra

Architektúra

A megvalósításunk legyen a következő:

Megvalósítás

Kiinduló projekt

🛠 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.

App Service

🛠 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

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:

Key Vault

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

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

🛠 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

Cosmos DB és Storage

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

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

🛠 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!

Klasszifikáció - Cognitive Service Custom Vision

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.

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

🛠 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:

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.

Kép kivágása

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.

Custom Vision

🛠 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.

Queue storage

🛠 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

Azure Function

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.

🛠 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.

Azure CDN

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:

🛠 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.

Application Insights

Track Exception

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;
}

Track Event

TODO

_telemetryClient.TrackEvent(
    "New pet added.",
    new Dictionary<string, string>
    {
        { "Pet type", pet.Type.ToString() },
    },
    new Dictionary<string, double>
    {
        { "New pet", 1 },
    });