Контейнери без головоломок: гайд із застосування Azure Container Apps

Підписуйтеся на Telegram-канал «DOU #tech», щоб не пропустити нові технічні статті

Привіт! Мене звати Віталій Дацишин, я працюю Associate .NET Architect в Intellias.
6 липня Intellias та Lviv .NET Community провели спільний захід — Charitable Lviv .NET Meetup. У межах події розглянули кілька цікавих тем, на базі яких і створена ця стаття — «Контейнери без головоломок: гайд із застосування Azure Container Apps». Тому сьогодні хочемо поділитися нею з читачами DOU в обмін на донати. Залишити подяку за контент та допомогти ЗСУ ви можете за посиланням.

І приємний бонус наприкінці: серед усіх, хто задонатить, 1 жовтня ми розіграємо подарунки — футболку від .NET community та екосумку від фонду Dzyga’s Paw.
Приємного читання!

Azure Container Apps

Azure Container Apps — це serverless платформа для деплою застосунків і мікросервісів, які загорнуті в контейнери. Основна перевага цієї платформи в тому, що ми можемо запускати контейнери, а складність конфігурації й оркестрації Azure Container Apps бере на себе. Під капотом тут використовується Kubernetes, тому ми маємо доступ до функцій, як-от Dapr, service discovery, менеджмент трафіку, динамічний scaling with KEDA, але в нас немає доступу безпосередньо до Kubernetes API. Serverless платформа дозволяє скейлити застосунки до 0 інстансів.

Purpose

Уявімо досить типовий випадок мікросервісної архітектури, коли є декілька сервісів, або, наприклад, був моноліт, який почав обростати мікросервісами. З самого початку ухвалили рішення використовувати App Service, бо він простий у навчанні й у використанні, туди легко задеплоїти .NET-додатки і він також підтримує контейнеризацію. Але кількість мікросервісів зростає і, відповідно, зростає потреба в оркестрації цих сервісів. Тому необхідні фічі, як-от service discovery, трафік менеджмент; також є певні ліміти зі скейлінгом: наприклад, scale out застосовується на весь App Service Plan, і нам може бракувати гранулярності скейлу, щоб скейлити тільки якісь окремі сервіси.

Як варіант, ми можемо використати Azure Kubernetes Service, який дає нам усе, що потрібно, та навіть більше. Деякі його фічі можуть бути просто не потрібні, але разом із цим він приносить набагато більшу складність із конфігурацією, ніж App Service. App Service дає простоту й мало контролю, тоді як AKS — багато контролю, але і багато складності у використанні.

І тут у гру вступає Azure Container Apps, який побудований на Kubernetes, і нам не потрібно займатися low level implementation деталями. У нас немає доступу безпосередньо до Kubernetes API, але нам доступні функції, як-от service discovery, traffic routing, ingress, скейлінг з KEDA, Dapr. І тому ACA лежать десь посередині цього компромісу між простотою використання і гнучкістю. Це дає нам достатній рівень контролю і дуже спрощує роботу з Kubernetes.

Scenarios

Cам Microsoft наводить декілька прикладів застосування Azure Container Apps:

  • Public API з підтримкою blue-green деплойменту, коли в нас є дві активні версії застосунків та можна менеджити трафік між ними, а також задати скейлінг правила на основі паралельних http-реквестів.
  • Background processing: наприклад, трансформація даних в БД або ETL-процес. У цьому випадку скейлінг може відбуватись на основі навантаженості CPU або пам’яті.
  • Event listener: тут скейл може бути на основі кількості меседжів у черзі.
  • Мікросервіси з використанням Dapr і KEDA для скейлінгу.

Key features + DEMO

Спочатку поглянемо на архітектуру простенького застосунку, який я зробив. У ньому дуже мало бізнес-логіки, і він створений для демонстрації можливостей ACA. Це Stressless App: він створений, аби покращувати користувачам настрій. У застосунку грає легка музика й у центрі екрана є пост із веселою картинкою і мотиваційним текстом або компліментом. Унизу сторінки користувач може залишити фідбек про те, чи допоміг йому застосунок.

У центрі архітектури Stressless App frontend. Для користувацького інтерфейсу використовуються прості razor pages. Через http-протокол Stressless App викликає Post Generator (ASP.NET Web API), який повертає йому текст компліменту й посилання на картинку. Також, коли користувач залишає фідбек на сторінці, то Stressless App паблішить меседж із цим фідбеком в Azure service bus, а service bus слухає сервіс Feedback Collector. Під капотом Feedback Collector використовує Azure Function, яка загорнута в docker-контейнер. Завдання цього сервісу оновлювати стан фідбеків. Blob Storage використовується для збереження цього стану.Також я додав правило, щоб Feedback Collector скейлився відповідно до кількості повідомлень у черзі. Кожен сервіс взаємодіє між собою через Dapr. Dapr використовує sidecar-патерн — це означає, що Dapr API запускається в окремому процесі й раниться поряд з нашим сервісом. І сам сервіс комунікує з ним через http- або grpc-протоколи. Тепер покроково створюватимемо цей застосунок.

Environments

Container Apps Environment — це своєрідний безпечний кордон навколо групи застосунків, які перебувають в одній віртуальній мережі й пишуть логи в одне й те саме місце. Azure бере на себе оновлення операційної системи, скейлінг, failover.

Причинами деплою контейнерів в одне середовище можуть бути ситуації, коли вам, наприклад, потрібно:

  • контролювати зв’язані сервіси;
  • деплоїти різні застосунки, але в одній віртуальній мережі; або розгорнути Container Apps Environment, коли у вас уже є віртуальна мережа;
  • викликати сервіси через Dapr service invocation API і ділити між цими сервісами конфігурацію Dapr.

Щодо ціни, ми будемо використовувати consumption план і, відповідно, від використаних ресурсів буде відбуватись нарахування оплати. Є ще dedicated, у якому фіксована ціна.

Тепер створимо наше середовище:

Спочатку оголошуємо змінні, які ми будемо використовувати. Далі створюємо ресурсну групу й Container Apps Environment. Там задаємо ім’я середовища, ресурс-групи та місцезнаходження. Також вказуємо internal-only false, щоб наш застосунок був доступний для користувачів інтернету:

$group = "StresslessApp"
$location = "westeurope"
$environment = "stressless-env"
$storage_account = "stresslessstorage1"
$servicebus_namespace = "stresslessservicebus1"
$queue = "feedback-queue"


# creating resource group
az group create --name $group `
--location $location


# creating environment
az containerapp env create --name $environment `
            --resource-group $group `
            --internal-only false `
            --location $location

Так виглядає наша ресурс-група. Створено Container Apps Environment, а також автоматично створено Log Analytics workspace:

Containers

Azure Container Apps менеджить деталі Kubernetes й оркестрації контейнерів за нас. Container Apps підтримують контейнери тільки на основі лінуксу, а також контейнери з будь-якого приватного чи публічного container registry. Ми будемо деплоїти їх як публічні в Docker Hub.

Також ми можемо ранити декілька контейнерів в одному Container App, наприклад, sidecar. Ці контейнери будуть ділити жорсткий диск, мережу й матимуть однаковий життєвий цикл, який ми розглянемо пізніше.

Dapr

Distributed Application Runtime — ця технологія надає API для спрощення взаємодії між мікросервісами. Тобто, по суті, ми можемо помістити нашу cross-cutting функціональність у Dapr.

Він складається з багатьох білдинг-блоків. У Stressless App використаємо service-to-service invocation, state management, publish / subscribe:

Service-to-service invocation

Розгляньмо цю частину архітектури:

Спочатку встановлюємо Dapr.AspNetCore NuGet пакет. І цей пакет також підтягне Dapr.Client NuGet пакет.

Щоб викликати Post Generator, cтворюємо Dapr-клієнт. У методі CreateInvokeMethodRequest ми передаємо як перший параметр тип http-методу. Другий параметр — це Dapr App ID, тобто при деплої Post Generator ми вказуємо його Dapr ID — і тепер зі Stressless фронтенду ми в такий спосіб викликаємо пост-генератор, а Dapr уже сам підставить правильний порт і лінк:

public async Task<IActionResult> Index()
{
    _logger.LogInformation("Calling Post Generator...");

    using var daprClient = new DaprClientBuilder().Build();
    var invokeMethod = daprClient.CreateInvokeMethodRequest(HttpMethod.Get, "stresslessapp-postgenerator", "PostGeneration");
    var post = await daprClient.InvokeMethodAsync<Post>(invokeMethod);

    _logger.LogInformation($"Received post compliment: '{post?.Compliment}'");

    ViewBag.Post = post;

    return View();
}

Третій параметр — це route до потрібного нам методу. Тож URL буде виглядати так:

State store

Тепер розглянемо збереження стану фідбеків:

Створюємо yml-файл, у якому описуємо наш state store та вказуємо потрібні метадані. Шаблони для різних компонентів можна знайти в документації Dapr:

componentType: state.azure.blobstorage
version: v1
metadata:
- name: accountName
  value: "STORAGE_NAME"
- name: accountKey
  secretRef: account-key
- name: containerName
  value: feedbacks
secrets:
- name: account-key
  value: "STORAGE_ACCOUNT_KEY"

У фідбек-колекторі, коли приходить новий фідбек, ми створюємо Dapr-клієнт. І пробуємо прочитати поточний стан feedback collection. Перший параметр state store — це Dapr-компонент імʼя, а другий — це ключ, за яким ми витягуємо state. І залежно від того, чи це позитивний чи негативний коментар, ми збільшуємо наш лічильник. Після цього зберігаємо оновлений стан:

using var daprClient = new DaprClientBuilder().Build();
var feedbackCollection = await daprClient.GetStateAsync<FeedbackCollection>("statestore", "FeedbackCollection") ?? new FeedbackCollection();

if (feedback.IsPositive)
{
    feedbackCollection.PositiveFeedbacksCount++;
}
else
{
    feedbackCollection.NegativeFeedbacksCount++;
}

await daprClient.SaveStateAsync("statestore", "FeedbackCollection", feedbackCollection);

Тепер задеплоїмо цей state store.

Насамперед створюю storage account:

# creating storage account
az storage account create --name $storage_account `
            --resource-group $group `
            --location $location `
            --sku Standard_RAGRS `
            --kind StorageV2

Далі читаю storage account-ключ і заміняю placeholders ключа й storage name на справжнє значення в state store yml-файлі:

<$storageKey = (az storage account keys list --account-name $storage_account --resource-group $group --output json --query "[0].value")

# replace secret placeholders
(Get-Content "statestore.yml").replace('"STORAGE_ACCOUNT_KEY"', $storageKey) | Set-Content "statestore.yml"
(Get-Content "statestore.yml").replace('STORAGE_NAME', $storage_account) | Set-Content "statestore.yml"

Потім прив’язую цей Dapr-компонент до Container Apps Environment і задаю його Dapr-компонент ім’я, через який ми і діставали наш state фідбеків:

# setting dapr state store
az containerapp env dapr-component set `
--name $environment --resource-group $group `
--dapr-component-name statestore `
--yaml '.\statestore.yml'

І наприкінці я назад заміняю значення ключа і storage name на placeholders:

# replace back secrets on placeholders
(Get-Content "statestore.yml").replace($storageKey, '"STORAGE_ACCOUNT_KEY"') | Set-Content "statestore.yml"
(Get-Content "statestore.yml").replace($storage_account, 'STORAGE_NAME') | Set-Content "statestore.yml"

Тепер в нашу ресурс-групу додався storage account:

І в Container Apps Environmentі з’явився Dapr-компонент:

Publish / Subscribe

Тепер розглянемо надсилання подій в Azure service bus.

Знову створюємо yml-файл для опису publish subscribe Dapr-компоненти:

componentType: pubsub.azure.servicebus.queues
version: v1
metadata:
- name: connectionString
  secretRef: connection-string
secrets:
- name: connection-string
  value: "CONNECTION_STRING"

У Dapr-клієнті викликаємо метод PublishEvent, куди як параметри передаємо ім’я Dapr-компоненту, назву черги та сам об’єкт, який ми хочемо передати. У такий же спосіб через Dapr-клієнт ми можемо читати меседжі з черги.

public async Task SendFeedbackAsync(bool isAppUseful)
{
    _logger.LogInformation($"Is this app useful? {isAppUseful}");

    using var daprClient = new DaprClientBuilder().Build();
    await daprClient.PublishEventAsync("pubsub", "feedback-queue", new Feedback { IsPositive = isAppUseful });

    _logger.LogInformation("Feedback published.");
}

Тепер це задеплоїмо. Спочатку створюємо service bus і чергу:

# creating service bus
az servicebus namespace create --resource-group $group --name $servicebus_namespace --location $location

az servicebus queue create --resource-group $group --namespace-name $servicebus_namespace --name $queue

Далі так само, як ми робили зі state store, в yml-файлі міняємо placeholder connection string на справжнє значення:

$serviceBusConnectionString = (az servicebus namespace authorization-rule keys list --resource-group $group --namespace-name $servicebus_namespace `
--name RootManageSharedAccessKey --query primaryConnectionString --output json)

# replace secret placeholders
(Get-Content "pubsub.yml").replace('"CONNECTION_STRING"', $serviceBusConnectionString) | Set-Content "pubsub.yml"

Потім створюємо Dapr-компонент:

# setting dapr pub/sub
az containerapp env dapr-component set `
--name $environment --resource-group $group `
--dapr-component-name pubsub `
--yaml '.\pubsub.yml'

Знову міняємо connection string на placeholder:

(Get-Content "pubsub.yml").replace($serviceBusConnectionString, '"CONNECTION_STRING"') | Set-Content "pubsub.yml"

На порталі в ресурс-групі з’явився service bus:

У Container Apps Environment з’явився pubsub Dapr-компонент:

Building docker images

Тепер ми можемо створити docker-зображення наших сервісів і запушити в Docker Hub:

# build images
docker build -t datsyshyn09/stresslessapp -f 'StresslessApp\Dockerfile' .
docker push datsyshyn09/stresslessapp

docker build -t datsyshyn09/stresslessappfeedbackcollector -f 'StresslessApp.FeedbackCollector\Dockerfile' .
docker push datsyshyn09/stresslessappfeedbackcollector

docker build -t datsyshyn09/stresslessapppostgenerator -f 'StresslessApp.PostGenerator\Dockerfile' .
docker push datsyshyn09/stresslessapppostgenerator

Deploying Container Apps

Опісля ми маємо можливість задеплоїти Container Apps. Почнемо з фронтенду.

На початку вказуємо ім’я застосунку, ресурc-групу та назву середовища. Далі — ім’я docker-image, порт застосунку й опісля вказуємо ingress. Для нашого фронтенду він external — це для того, аби доступ до застосунку був у всіх, у кого є інтернет. Лімітом ACA є те, що ви можете розкрити тільки один порт, один ingress і неважливо — чи це external, чи internal. Тобто застосунок не може одночасно мати відкриті порти для, наприклад, веббраузера і меседж-брокера. Далі вказуємо мінімальну й максимальну кількість реплік. Оскільки тут мінімум реплік 0, то наш застосунок, можна сказати, serverless. Він заскейлиться до нуля, адже певний час запитів не буде, і, відповідно, перший старт займе трохи часу, поки застосунок оживе.

Опісля ми зазначаємо, що будемо використовувати Dapr. Задаємо environment variables. Далі вказуємо для Dapr, на якому порті наш застосунок, і задаємо Dapr ID. Наприклад, завдяки цьому Dapr ID ми робимо service-to-service invocation:

# creating the StresslessApp
az containerapp create `
--name stresslessapp `
--resource-group $group `
--environment $environment `
--image datsyshyn09/stresslessapp:latest `
--target-port 80 `
--ingress 'external' `
--min-replicas 0 `
--max-replicas 5 `
--enable-dapr `
--env-vars ASPNETCORE_ENVIRONMENT="Development" `
--dapr-app-port 80 `
--dapr-app-id stresslessapp

За аналогією деплоїмо Post Generator. Відмінність у тому, що там — internal ingress, тобто цей сервіс доступний тільки для тих сервісів, що перебувають усередині Container Apps Environment:

# creating the StresslessApp.PostGenerator
az containerapp create `
--name stresslessapp-postgenerator `
--resource-group $group `
--environment $environment `
--image datsyshyn09/stresslessapppostgenerator:latest `
--target-port 80 `
--ingress 'internal' `
--min-replicas 0 `
--max-replicas 5 `
--enable-dapr `
--env-vars ASPNETCORE_ENVIRONMENT="Development" `
--dapr-app-port 80 `
--dapr-app-id stresslessapp-postgenerator

І тепер такий вигляд має наша ресурс-група:

Перед тим, як деплоїти фідбек-колектор, давайте розберемося з KEDA.

KEDA

Kubernetes Event-Driven Autoscaling дозволяє скейлити будь-який контейнер від нуля до потенційно тисяч інстансів на основі кількості меседжів в Azure-черзі або kafka-стрімі і ще на основі багатьох меседж-брокерів, які підтримує KEDA. Також Azure Container Apps підтримує скейлінг на основі паралельних http-запитів.
Ми хочемо додати скейлінг-правило для нашого Feedback Collector, щоб робити scale out, якщо в service bus queue буде 5 меседжів.
Спочатку заходимо в документацію KEDA і шукаємо service bus:

Після цього в деплоймент-скрипт додаємо секрет на connection string service bus, а також скейл-правило. Спочатку даємо йому ім’я, наприклад я назвав azure-servicebus-queue-rule, потім указуємо тип, який беремо з документації KEDA, — azure-servicebus і вказуємо метадані (такі, як зазначено в документації). Ми додали ім’я черги, namespace service bus і кількість меседжів для скейлу. Також вказуємо, що ми будемо аутенфікуватись через connection string, який записаний у наш secret, і додаємо connection string як env variable, оскільки в нас під капотом Azure Function з тригером на нашу чергу. Усю решту ми робимо за аналогією з попередніми сервісами:

# creating the StresslessApp.FeedbackCollector
az containerapp create `
--name stresslessapp-feedbackcollector `
--resource-group $group `
--environment $environment `
--image datsyshyn09/stresslessappfeedbackcollector:latest `
--target-port 80 `
--ingress 'internal' `
--min-replicas 0 `
--max-replicas 5 `
--enable-dapr `
--secrets connection-string=$serviceBusConnectionString `
--scale-rule-name azure-servicebus-queue-rule `
--scale-rule-type azure-servicebus `
--scale-rule-metadata queueName=$queue `
            namespace=$servicebus_namespace `
            messageCount=5 `
--scale-rule-auth connection=connection-string `
--env-vars ASPNETCORE_ENVIRONMENT="Development" ConnectionString=secretref:connection-string `
--dapr-app-port 80 `
--dapr-app-id stresslessapp-feedbackcollector

Після деплойменту ось такий вигляд має наша ресурс-група:

Results

Stressless App готовий до використання. Такий вигляд має інтерфейс застосунку:

Перевіряємо логи, що Post Generator успішно повертає комплімент і лінк на картинку, а також те, що фідбеки успішно потрапляють у чергу:

Тут логи з Feedback Collector, що меседжі з черги успішно опрацьовані та стан фідбеків оновлено:

І тут сам blob Feedback Collection і його стан:

Revisions

Azure Container Apps реалізують версіонування застосунків за допомогою ревізій. Ревізія — це immutable, тобто незмінний снепшот версії Container Apps.

  • Перша ревізія створюється автоматично при деплої Container Apps.
  • Нові ревізії створюються автоматично, якщо ми змінюємо revision scope нашого застосунку або оновлюємо ревізії, що вже існують.
  • Якщо ми зробимо зміни в application scope, то ці зміни матимуть вплив на всі ревізії незважаючи на те, що ревізії іммутабельні.

Revision Scope:

  • container / image;
  • scale rules.

Application Scope:

  • secrets;
  • ingress configuration;
  • Dapr settings;
  • credentials for private container registries;
  • revision mode.

Є два режими для ревізій: single і multiple.
Single означає, що може бути тільки одна активна ревізія або версія застосунку.
Multiple означає, що в нас одночасно може бути активними декілька ревізій і ми можемо сплітати трафік між ними. Це може бути корисним для таких деплоймент-стратегій, як-от blue-green або A / B testing, а також аби швидко ревертнути версію, якщо з нею щось не так.

Я змінив режим на multiple і змінив скейл-правило (тепер максимальна кількість інстансів 7, а не 5), щоб утворилась нова ревізія. Тепер ми можемо ділити трафік між ними:

Також кожна ревізія має свій URL (на цьому скріншоті у графі Revision URL): тобто ми можемо відкрити і попередні версії нашого застосунку:

Application lifecycle

Тепер розглянемо application lifecycle, який тісно пов’язаний з ревізіями. Є чотири фази життя застосунку.

Перша — коли ми вперше деплоїмо застосунок, і першу ревізію буде створено автоматично:

Друга — коли ми оновлюємо revision scope застосунку, і створюється нова ревізія. І залежно від обраного режиму, попередня версія або автоматично деактивується, або обидві версії будуть працювати паралельно:

Третій етап — це деактивація ревізії. Ми можемо вручну деактивувати ревізію, якщо вона більше не потрібна, або старі версії застосунку будуть автоматично деактивовані, якщо ми обрали single mode:

І четверта фаза — це, власне, shutdown ревізії. Він настає, якщо:

  • Container App робить scale in, тобто зменшує кількість інстансів;
  • весь Container App видалений;
  • після деактивації ревізії.

Jobs (preview)

Наостанок розглянемо нову фічу Container Apps, яка була ще у прев’ю, — Jobs.

Container Apps, які ми вже використовували, раняться постійно, якщо ми їх не скейлимо до 0. І якщо застосунок упаде, то він рестартанеться автоматично. Можна спрощено порівняти їх з Azure App Service, зазвичай їх використовують для вебзастосунків.

А Jobs — це таски, що мають часовий ліміт на виконання, здійснюють якусь одноразову роботу, наприклад, зберігають state у blob storage.

Jobs мають три типи тригерів:

  • мануальний, коли ми Jobs тригеримо на вимогу;
  • за розкладом, коли Jobs раниться в певний заданий час;
  • подієвий, коли, наприклад, у чергу приходить івент, і щоб його запроцесити, Jobs прокидається.

Але я не використовував Jobs у Stressless застосунку, тому що вони мають певні обмеження. Наприклад, вони не підтримують Dapr, ingress і посилання на KeyVault secret.

Корисні посилання:

Дякуємо всім, хто дочитав статтю та залишив донат!
Якщо ви ще не встигли задонатити на наш збір, дублюємо посилання.

👍ПодобаєтьсяСподобалось12
До обраногоВ обраному7
LinkedIn
Дозволені теги: blockquote, a, pre, code, ul, ol, li, b, i, del.
Ctrl + Enter
Дозволені теги: blockquote, a, pre, code, ul, ol, li, b, i, del.
Ctrl + Enter

Підписатись на коментарі