Backend

Sprawdziliśmy, czy Azure Functions 2.0 jest gotowe do wdrożeń produkcyjnych

Pod koniec września 2018 roku Microsoft ogłosił wydanie Azure Functions 2.0 GA. Nowe funkcjonalności i wprowadzone usprawnienia zrobiły na nas duże wrażenie. Eduardo Laureano, Principal Program Manager odpowiedzialny w Microsoft za technologię serverless, napisał na swoim blogu: Azure Functions 2.0 is production ready and capable of handling your most demanding workloads, backed by our 99.95 percent SLA. W zespole LAB Altkom Software & Consulting uznaliśmy, że to dobry moment na zrobienie czegoś więcej niż “Hello World”.


Robert Witkowski. Starszy Programista w zespole ASC LAB. Zafascynowany możliwościami sztucznej inteligencji i uczenia maszynowego. Lubiący wyzwania, nowe technologie i dobrze zaprojektowane rozwiązania. Aktualnie odpowiedzialny za działania R&D oraz propagowanie dobrych praktyk wewnątrz organizacji.

Wojciech Suwała. Lider zespołu ASC LAB. Architekt z wieloletnim doświadczeniem w budowaniu złożonych rozwiązań dla biznesu w Javie i .NET. W swojej codziennej pracy buduje systemy z wykorzystaniem architektury mikroserwisowej z użyciem Domain Driven Design.


Dlaczego serverless ma znaczenie?

Od wielu lat programiści nauczani są projektowania systemów z luźno powiązanych, niezależnych komponentów. Są one następnie wdrażane na jedną, wielką maszynę, przez co tracą pierwotną niezależność i możliwość odrębnego skalowania.

Architektura serverless to powrót do korzeni, do dobrych praktyk projektowania systemów. Dzięki niej możemy tworzyć nasz system wykorzystując małe, autonomiczne komponenty, które skalują się niezależnie i każdy z nich może mieć swoje wymagania dotyczące wydajności i skalowalności. Co więcej — ta koncepcja jest uzasadniona również ekonomicznie. Wykorzystując serverless, płacimy tylko za zasoby (CPU, RAM), których faktycznie używamy.

Kolejną zaletą tego podejścia jest to, że nie musimy zarządzać infrastrukturą. Nie ma potrzeby konfiguracji maszyn wirtualnych czy instalacji/aktualizacji systemów operacyjnych. Tym wszystkim zajmuje się dostawca rozwiązań serverless. Dodatkowo udostępnia rozbudowane narzędzia umożliwiające monitoring czy auto-skalowanie.

Najważniejsze funkcjonalności Azure Functions

Funkcjonalnościami, o których na pewno warto wspomnieć podczas pisania o Azure Functions 2.0 są:

  • Wybór języka – funkcję możemy pisać używając jednego z wielu językach: C#, F#, Node.js, Java, PHP, batch, bash.
  • Płatność tylko za to, co faktycznie zużyto (ang. pay-per-use pricing model) – jeśli funkcja się aktualnie nie wykonuje – nic nie płacimy.
  • Zarządzanie zależnościami – platforma wspiera rozwiązania do zarządzania pakietami takie jak NuGet i NPM, dzięki czemu możemy używać naszych ulubionych bibliotek.
  • Wbudowana integracja z mechanizmami bezpieczeństwa – funkcje wzbudzane przez żądanie HTTP możemy w prosty sposób zintegrować z dostawcą OAuth takim jak Azure Active Directory, Facebook, Google, Twitter lub Microsoft Account.
  • Łatwa integracja z innymi usługami AzureAzure Functions posiadają wbudowane mechanizmy do integracji z wieloma innymi usługami dostępnymi w chmurze Microsoftu lub udostępnionymi jako SaaS (np.: SendGrid, Twilio).
  • Możliwość rozwoju na wiele sposobów – kod funkcji dostępny jest bezpośrednio w portalu, gdzie możemy go edytować. W prosty sposób możemy skonfigurować proces ciągłej integracji i wdrażania (continuous integration & continuous deployment) i wdrażać nasze funkcje z GitHuba, lokalnego serwera Git czy wykorzystując Visual Studio Team Services.
  • Open-source – runtime funkcji jest ogólnodostępny na GitHubie.
  • Możliwość wdrażania funkcji “on-premise” – nie jesteśmy uzależnieni od Azure, możemy uruchamiać funkcję na własnej infrastrukturze.

Biznesowy przypadek użycia

Analizując różne możliwości użycia architektury serverless, stwierdziliśmy, że z biznesowego punktu widzenia dobrym przykładem jest system billingowy dla firm, które oferują swoje usługi w modelu subskrypcyjnym.

Jak ma działać taki system?

Klient przesyła do firmy listę pracowników, którzy będą korzystać z danej usługi (np. karty MultiSport). Na podstawie cen w kontrakcie, system oblicza opłatę dla każdego pracownika. Następnie opłaty za dany okres rozliczeniowy agregowane są w jedną fakturę dla klienta, która w formie PDFa zapisywana jest w konkretnym, ustalonym miejscu. Dodatkowo każdy klient informowany jest drogą mailową i SMSową o wystawieniu nowej faktury.

Poniższy diagram przedstawia architekturę opracowanego rozwiązania. Kod źródłowy i poradnik jak uruchomić to rozwiązanie lokalnie dostępny jest na naszym koncie na GitHubie.

1. Użytkownik wrzuca plik CSV z listą beneficjentów (pracowników klienta) w ustalone wcześniej miejsce – technicznie jest to Azure Blob Container.

2. Powyższa akcja wyzwala wywołanie funkcji GenerateBillingItemsFunc, która odpowiedzialna jest za:

  • Wygenerowanie elementów rozliczeniowych (billing items). Używane są do tego ceny zdefiniowane w zewnętrznej bazie danych (CosmosDB). Elementy zapisywane są w tabeli (Azure Table).
  • Wysłanie wiadomości do kolejki (Azure Queue) informującej inne elementy o wygenerowaniu nowych elementów rozliczeniowych.

3. Pojawienie się nowej wiadomość na określonej kolejce powoduje uruchomienie następnej funkcji w procesie (GenerateInvoiceFunc). Funkcja ta tworzy obiekt domenowy faktury (Invoice) i zapisuje go w bazie danych (CosmosDB). Po udanym zapisie wysyłane są wiadomości do kolejnych kolejek — jedna o konieczności wygenerowania PDFa, a druga o konieczności poinformowania klienta poprzez wysłanie e-maila oraz SMSa.

4. Pojawienie się wiadomości na kolejce invoice-print-request powoduje uruchomienie PrintInvoiceFunc. Funkcja używa zewnętrznego silnika do generowania plików PDF na podstawie zdefiniowanych szabloów — JsReport. Wygenerowany plik zapisywany jest w Azure Blob Storage.

5. Pojawienie się wiadomości na kolejce invoice-notification-request powoduje uruchomienie NotifyInvoiceFunc. Funkcja używa dwóch zewnętrznych systemów — SendGrid’a do wysyłania e-maili oraz Twilio do wysyłania SMS’ów.

Tworząc powyższe rozwiązanie staraliśmy się przestrzegać najlepszych praktyk, m.in. tego, że funkcje powinny być małe, proste i działać niezależnie.

Przetestowaliśmy dwa podejścia do tworzenia zestawu funkcji:

1. jeden projekt == jedna funkcja

2. wszystkie funkcje w jednym projekcie

AllInOneProject vs. rozdzielone funkcje

Należy zauważyć, że wybór podejścia wpływa na ilość stworzonych Function App, a co za tym idzie poziomu niezależności poszczególnych komponentów. Cytując dokumentację Azure wyjaśniającą, czym jest Function App:

A function app provides an execution context in Azure in which your functions run. A function app consists of one or more individual functions that are managed together by Azure App Service. All of the functions in a function app share the same pricing plan, continuous deployment and runtime version. Think of a function app as a way to organize and collectively manage your functions.

Jeśli wybierzemy pierwsze podejście, wszystkie nasze funkcje będą dzielić ten sam cennik (pricing plan), wersję środowiska uruchomieniowego oraz będą musiały być wdrażane razem.

Drugie podejście pozwala rozdzielić to wszystko — każda funkcja może korzystać z innego planu, może mieć inne środowisko uruchomieniowe i być wdrażana osobno.

Jeśli interesują Cię szczegóły i dodatkowe informacje, polecam świetny artykuł Marc’a Duiker’a.

W poniższych sekcjach skupimy się na dokładniejszym opisie budowy każdej funkcji.

Generowanie rozliczeń (GenerateBillingItemsFunc)

Główną odpowiedzialnością tej funkcji jest parsowanie przesłanego pliku CSV i wygenerowanie na jego podstawie odpowiednich elementów rozliczeniowych.

[FunctionName("GenerateBillingItemsFunc")]
public static void Run(
    [BlobTrigger("active-lists/{name}", Connection = "AzureWebJobsStorage")] Stream myBlob, string name,
    [Table("billingItems")] out ICollector billingItems,
    [Queue("invoice-generation-request")] out InvoiceGenerationRequest queueRequest,
    ILogger log)
{
    log.LogInformation($"C# Blob Trigger function Processed blob: {name} Bytes");

    var activeList = ActiveListParser.Parse(name, myBlob);
    var generator = new BillingItemGenerator();
    var priceList = GetPriceList(activeList.CustomerCode);
    foreach (var bi in generator.Generate(activeList, priceList))
    {
        billingItems.Add(bi);
    }

    queueRequest = InvoiceGenerationRequest.ForActiveList(activeList);
}

Dzięki atrybutowi [BlobTrigger], funkcja uruchamia się automatycznie w momencie przesłania przez użytkownika pliku CSV do kontenera Blob Storage nazwanego active-lists (parametr ten możemy skonfigurować przez zmianę Connection).

Nazwa przesłanego pliku musi być kompatybilna ze wzorcem: [KOD_KLIENTA]_[ROK]_[MIESIAC]_*, przykładowo: ASC_2018_11_activeList.txt.

Przykładowy plik:

99050555745;Annaliese Verena;A
29120458762;Josepha Gusti;A
39091666028;Deborah Wenzi;B
77050929111;John Smith;A
76091166752;Bob Martin;A
97031653569;Alice Smith;B
35060205229;Patricia Glide;A
38112669875;Mike Kowalski;B
13102408939;Kali Mali;A

W każdej linii zapisany jest: PESEL, imię i nazwisko, kod produktu.

Na podstawie pierwszej części nazwy pliku (kodu klienta) funkcja wie, z jakich cen powinna skorzystać podczas generowania elementów rozliczeniowych.

Sposób parsowania pliku oraz pobierania cen z bazy danych nie jest tu kluczowy, ale kogoś interesują szczegóły, warto zerknąć w ActiveListParser.cs oraz PriceRepository.cs.

Dzięki atrybutowi [Table] możemy zapisać informacje w Azure Table Storage. Dodanie nowego rekordu do tabeli to po prostu wywołanie na kolekcji metody add – billingItems.Add().

Dzięki atrybutowi [Queue] możemy wysłać wiadomość na kolejkę Azure Queue.

Generowanie faktury (GenerateInvoiceFunc)

Po wygenerowaniu elementów rozliczeniowych, powinniśmy wygenerować fakturę dla naszych klientów. GenerateInvoiceFunc przygotowuje i zapisuje w bazie danych obiekt faktury i wysyła informacje o tym do odpowiednich kolejek.

[FunctionName("GenerateInvoiceFunc")]
public static void Run(
    [QueueTrigger("invoice-generation-request")] InvoiceGenerationRequest request,
    [Table("billingItems")] CloudTable billingItems,
    [CosmosDB("crm", "invoices", ConnectionStringSetting = "cosmosDb")] out dynamic generatedInvoice,
    [Queue("invoice-print-request")] out InvoicePrintRequest printRequest,
    [Queue("invoice-notification-request")] out InvoiceNotificationRequest notificationRequest,
    ILogger log)
{
    log.LogInformation($"C# Queue trigger function processed: {request.CustomerCode} {request.Year} {request.Month}");

    var generator = new InvoiceGenerator();
    var items = GetBillingItemsFromTable(billingItems, request);
    var invoice = generator.Generate(request, items);

    generatedInvoice = invoice;
    
    printRequest = new InvoicePrintRequest { InvoiceToPrint = invoice };
    notificationRequest = new InvoiceNotificationRequest { InvoiceForNotification = invoice };
}

Dzięki atrybutowi [QueueTrigger] funkcja uruchamia się automatycznie po tym, jak na kolejce invoice-generation-request pojawi się nowa wiadomość.

Dzięki atrybutowi [Table] funkcja ma dostęp do tabeli, w której zostały zapisane elementy rozliczeniowe (poprzedni krok).

W funkcji używamy napisanej przez nas GetBillingItemsFromTable, która na podstawie danych z request’u potrafi pobrać interesujący nas zestaw danych:

static List GetBillingItemsFromTable(CloudTable billingItems, InvoiceGenerationRequest request)
{
    TableQuery query = new TableQuery()
        .Where(
            TableQuery.GenerateFilterCondition("PartitionKey", QueryComparisons.Equal, $"{request.CustomerCode}-{request.Year}-{request.Month}")
        );

    var querySegment = billingItems.ExecuteQuerySegmentedAsync(query, null);
    var items = new List();
    foreach (BillingItem item in querySegment.Result)
    {
        items.Add(item);
    }
    return items;
}

Atrybut [CosmosDB] pozwala wykorzystać bazę danych CosmosDB do zapisania stworzonego obiektu faktury.

Ostatnie dwa atrybuty [Queue] pozwalają wysłać odpowiednie wiadomości na kolejne kolejki — dzięki nim informujemy kolejne komponenty (elementy procesu) o konieczności stworzenia PDFa i wysłania powiadomień do użytkownika.

Generowanie PDFa z fakturą (PrintInvoiceFunc)

Ta funkcja odpowiedzialna jest za stworzenie faktury w formacie PDF na podstawie konkretnego szablonu i zapisanie go w odpowiednim miejscu:

[FunctionName("PrintInvoiceFunc")]
public static void Run(
    [QueueTrigger("invoice-print-request")]InvoicePrintRequest printRequest,
    Binder binder,
    ILogger log)
{
    var jsReportUrl = Environment.GetEnvironmentVariable("JsReportUrl");
    var pdf = new InvoicePrinter(jsReportUrl).Print(printRequest.InvoiceToPrint);

    StoreResultInBlobAsync(
        binder,
        $"Invoice_{printRequest.InvoiceToPrint.InvoiceNumber.Replace("/","_")}",
        pdf);
}

Atrybut [QueueTrigger] został już wcześniej opisany — dzięki niemu funkcja uruchamiana jest za każdym razem, gdy na wybranej kolejce pojawi się nowa wiadomość.

Proces tworzenia PDFa został oddelegowany do zewnętrznego systemu – JS Report. Dzięki obiektowi Binder użytemu jako kolejny parametr funkcji, możemy asynchronicznie zapisać stworzonego PDFa we wskazanym magazynie (Azure Blob Storage). Odpowiada za to poniższa metoda:

private static async Task StoreResultInBlobAsync(Binder binder, string title, byte[] doc)
{
    using (var stream = await binder.BindAsync(new BlobAttribute($"printouts/{title}.pdf", FileAccess.Write)))
    {
        using (var writer = new BinaryWriter(stream))
        {
            writer.Write(doc);
        }
    }
}

Wysyłanie powiadomień (NotifyInvoiceFunc)

Równolegle do procesu tworzenia PDFa faktury, uruchamiana jest funkcja wysyłająca powiadomienia do użytkownika:

//Better name: SendNotificationFunc
[FunctionName("NotifyInvoiceFunc")] 
public static void Run(
    [QueueTrigger("invoice-notification-request")] InvoiceNotificationRequest notificationRequest,
    [SendGrid(ApiKey = "SendGridApiKey")] out SendGridMessage email,
    [TwilioSms(AccountSidSetting = "TwilioAccountSid", AuthTokenSetting = "TwilioAuthToken", From = "+15005550006")] out CreateMessageOptions sms,
    ILogger log)
{
    log.LogInformation($"C# Queue trigger function processed: {notificationRequest}");

    email = CreateEmail(notificationRequest);
    sms = CreateSMS(notificationRequest);
}

Dzięki wbudowanym mechanizmom Azure Functions, integracja z Twilio (wysyłka SMS) i SendGridem (wysyłka email) przebiega bez żadnych problemów i nadmiernej konfiguracji.

Żeby wysłać email za pomocą SendGrida, musimy użyć atrybutu [SendGrid], wewnątrz którego definiujemy APIKey. Przypisanie nowego obiektu SendGridMessage do parametru wejściowego spowoduje wysłanie e-maila. Za tworzenie tego obiektu odpowiada metoda CreateEmail:

private static SendGridMessage CreateEmail(InvoiceNotificationRequest request)
{
    var email = new SendGridMessage();

    email.AddTo("CUSTOMER_EMAIL@example.com");
    email.AddContent("text/html", $"You have new invoice {request.InvoiceForNotification.InvoiceNumber} for {request.InvoiceForNotification.TotalCost.ToString()}.");
    email.SetFrom(new EmailAddress("YOUR_EMAIL@example.com"));
    email.SetSubject($"New Invoice - {request.InvoiceForNotification.InvoiceNumber}");

    return email;
}

Wysyłka SMSów przez Twilio odbywa się w bardzo podobny sposób. Dzięki atrybutowi [TwilioSms] możemy zintegrować swoje konto Twilio z napisaną funkcją. Właściwość from w naszym przykładzie wypełniona jest tzw. magicznym numerem, dostępnym w dokumentacji.

private static CreateMessageOptions CreateSMS(InvoiceNotificationRequest request)
{
    return new CreateMessageOptions(new PhoneNumber("+15005550006"))
    {
        Body = $"You have new invoice {request.InvoiceForNotification.InvoiceNumber} for {request.InvoiceForNotification.TotalCost.ToString()}."
    };
}

Jak monitorować?

Dzięki integracji z inną usługą dostępną na Azure — Azure Application Insight, możemy bardzo dokładnie monitorować nasze funkcje i obserwować ich wyniki. Dzięki wizualizacji w postaci mapy możemy zrozumieć, w jaki sposób poszczególne komponenty komunikują się ze sobą, a wchodząc w szczegóły możemy diagnozować usterki na poziomie konkretnych wywołań.

Application Map w przypadku wdrożenia funkcji jako jedna Function App (bez separacji)

Application Map w przypadku wdrożenia każdej z funkcji jako oddzielna Function App (z separacją)

Dzięki widokowi “End-to-end transaction details” jesteśmy w stanie monitorować każde wywołanie funkcji i analizować na przykład problemy wydajnościowe.

Podsumowanie

Zaprezentowany przykład pokazuje, że Azure Functions 2.0 to technologia, która jest gotowa do wdrożeń produkcyjnych (production-ready). W kilku poniższych punktach postaramy się wymienić największe zalety i wady rozwiązań opartych o architekturę serverless i zbudowanych za pomocą Azure Functions 2.0.

Nasze doświadczenia (jako programistów) są naprawdę świetne. W prostu sposób możemy budować i testować całe rozwiązanie lokalnie, a później dosłownie kilkoma kliknięciami wdrożyć na Azure. Platforma posiada wbudowaną integrację do wielu innych serwisów/zasobów dostępnych w Azure, np: Azure Blob, Azure Table, Azure Queue, bazy danych (CosmosDB), jak i zewnętrznych serwisów takich jak SendGrid czy Twilio. Zdejmuje to z programistów konieczność ręcznego zarządzania połączeniami i zwalniania zasobów, a dodatkowo bardzo upraszcza kod.

Narzędzia do monitoringu są naprawdę wysokiej jakości i pomagają szybko diagnozować różnego typu problemy.

Zalety Azure Functions 2.0

  • promuje dobre praktyki projektowania oprogramowania;
  • pozwala skupić się programistom na pisaniu małych, autonomicznych komponentów, które wpisują się w dobre praktyki OOP i SOLID;
  • posiada autoskalowanie i monitoring jako funkcjonalności dostępne praktycznie bez żadnej dodatkowej konfiguracji;
  • płaci się tylko za zasoby, które faktycznie się zużywa;
  • platforma pozwala bierze na siebie cały ciężar związany z zarządzaniem serwerami, maszynami wirtualnymi, kontenerami;
  • dzięki wbudowanym mechanizmom integracja z wieloma innymi technologiami jest szybka i prosta (kolejki, bazy danych, bloby, systemy zewnętrzne tj. Twilio, SendGrid);

Wady Azure Functions 2.0

  • tzw. “magiczna infrastruktura” zwiększa ryzyko problemów przy diagnozowaniu problemów integracyjnych;
  • nadal występuje problem zimnego startu (cold start);
  • wbudowane mechanizmy integracji są związane głównie z innymi usługami Azure, co może skutkować silnymi zależnościami między naszym rozwiązaniem a platformą tzw. vendor lock-in (ale zawsze możemy użyć trigger’ów HTTP, żeby zintegrować cokolwiek innego 🙂 );
  • dużo mniejsza kontrola nad aplikacjami wymaga przemyślenia mechanizmów wokół sesji i uwierzytelniania użytkowników;
  • konfiguracja z czasem może stawać się dużo bardziej złożona;
  • kontrola kosztów nadal nie jest perfekcyjna (nadzieje wzbudza jej poprawa z wersji na wersje);
  • wszystkie rozwiązania serverless cierpią na pewne problemy wieku dziecięcego, więcej o nich można poczytać tutaj.

najwięcej ofert html

 

Podobne artykuły

[wpdevart_facebook_comment curent_url="https://geek.justjoin.it/sprawdzilismy-czy-azure-functions-2-0-jest-gotowa-do-wdrozen-produkcyjnych/" order_type="social" width="100%" count_of_comments="8" ]