kpi

Magazín KPI

№ 6 / máj 2019

Serverless

Serverless je zaujímavou alternatívou k tradičným architektúram, ktorá využíva princíp dekompozície problému na veľké množstvo drobných častí — funkcií, ktoré spoločne poskytujú celkové riešenie. Tieto funkcie bežia v prostredí cloudu, ktoré zabezpečuje ich automatické škálovanie. V tomto článku si rozoberieme cestu, ktorá nás viedla k architektúre Serverless, a na demo aplikácii si ukážeme, ako jednoducho a rýchlo dokážeme pomocou nej vyvinúť zaujímavú dynamickú webovú stránku.

Definícia Serverless

Serverless je pojem, ktorý sa prvotne spája so single-page webovými aplikáciami, ktoré využívali služby BaaS (Backend as a Service), a teda namiesto písania vlastného serverového kódu, plne využívali databázové, autentifikačné a iné služby poskytované nejktorou platformou (napríklad Firebase alebo Parse, o ktorom sme už písali). Postupne sa ale tento pojem začal využívať v súvislosti s implementáciou serverového kódu, ktorý beží v bezstavovom prostredí platformy FaaS (Function as a Service). Zvyčajne sa táto platforma využíva ako jedna zo služieb cloudového poskytovateľa, kde v spojení s inými službami vytvára robustný ekosystém pre vývoj aplikácií. Príkladom takejto služby sú napríklad AWS Lambda, či Azure Functions.

Cesta k Serverless architektúre

Vráťme sa ale o niekoľko desiatok rokov späť, kedy bolo úplne bežné hostovať si webové stránky samostatne, za pomoci vlastného hardvéru, častokrát priamo doma. Aj keď to možno na prvý pohľad nemusí byť zrejmé, takýto prístup so sebou prináša mnohé problémy, riziká a komplikácie. Je potrebné vyriešiť zálohovanie, redundanciu, postupne meniť hardvérové komponenty, či už v prípade poruchy alebo prechodu na výkonnejšie zariadenie. Okrem týchto problémov tiež musíme počítať s nečakanými výpadkami internetu a elektriny, ktoré ohrozia nepretržitú dostupnosť nášho servera.

Riešenie pre tieto problémy ponúka platforma IaaS, teda Infrastructure as a Service. Tá nám poskytuje možnosť prenajať si hardvér, najčastejšie v podobe virtuálneho stroja, ktorý máme plne pod kontrolou. Naše prvotné problémy berie na seba poskytovateľ tejto platformy a my sa posúvame o úroveň abstrakcie vyššie. Na úvod je našou úlohou inštalácia operačného systému, za ktorou nasleduje konfigurovanie jednotlivých služieb a nastavenie bezpečnosti nášho systému. Aby sme zabezpečili bezproblémové fungovanie našich služieb, potrebujeme okrem iného pravidelne aplikovať bezpečnostné záplaty.

Keďže sa ani tieto starosti priamo netýkajú nášho poslania — riešenia doménových problémov prostredníctvom kódu — najrozumnejším riešením je prenechať ich niekomu inému. Pre tieto účely existuje služba PaaS, teda Platform as a Service. Tá ponúka možnosť priameho nahrania kódu (v podporovanom jazyku / technológii) ktorý sa vykonáva v prostredí na to určenom. Príkladom takejto služby je Heroku, o ktorom sme písali v predchádzajúcich číslach Magazínu. Všetky problémy týkajúce sa hardvérovej konfigurácie, nainštalovaného operačného systému, potrebných služieb či zabezpečenia rieši poskytovateľ tejto platformy za nás. Našim jediným problémom sa stáva zabezpečenie správneho škálovania, čo znamená, že potrebujeme zabezpečiť primeranú alokáciu hardvérových zdrojov vzhľadom na aktuálne vyťaženie našej aplikácie. To sa zvyčajne vykonáva prostredníctvom webového portálu poskytovateľa. Nesprávne zvládnutie tohto problému prináša zbytočné výdavky na alokované hardvérové zdroje v prípade nízkeho vyťaženia, či neschopnosť obslúžiť všetkých používateľov v čase špičky.

Úlohou platformy FaaS, teda Function as a Service, je manažovať interné (hardvérové a softvérové) zdroje. Našu aplikáciu vyvíjame ako súhrn autonómnych funkcií, ktoré spoločne zabezpečujú potrebnú funkcionalitu. Rozbitím našej architektúry na množstvo malých častí zabezpečíme lepšiu podporu škálovania. To sa vykonáva automaticky, vzhľadom na vyťaženosť aplikácie. Škálovanie je možné zabezpečiť vertikálne — pridaním alebo odobratím hardvérových zdrojov pre konkrétnu funkciu — ale najvyšší dôraz je kladený na škálovanie do šírky.

Horizontálne škálovanie funguje na princípe pridávania a odoberania inštancií jednotlivej funkcie a teda je (teoreticky) možné škálovať neobmedzene. Pre dosiahnutie takýchto cieľov je potrebné správne navrhnúť architektúru našej aplikácie a striktne dodržiavať zásady pre vývoj samotných funkcií.

Naše funkcie by mali byť:

  • malé — každá funkcia by mala riešiť jeden konkrétny doménový problém,
  • samostatné — funkcia by nemala volať iné funkcie,
  • štíhle — používať čo najmenej knižníc,
  • asynchrónne a bezstavové — tok dát medzi funkciami by mal prebiehať cez frontu správ (messaging queue) a databázu (napr. DynamoDB, CosmosDB, Aurora Serverless).

Pre získanie detailnejších informácií o architektúre Serverless, odporúčam tieto zaujímavé články:

Poskytovatelia FaaS

Architektúra serverless sa najčastejšie využíva v prostredí cloudu. Najpolulárnejšie možností sú:

V prípade, že chceme mať aj naďalej pod kontrolou infraštruktúru alebo je našou prioritou väčšia flexibilita pri finálnom nasadení našej FaaS aplikácie, je vhodné využiť platformu Open FaaS. Tá pre svoje fungovanie využíva kontajnery Docker a ako orchestrátor je možné využiť Kubernetes alebo Docker Swarm.

Demo aplikácia

Aby sme si ukázali tvorbu takejto serverless aplikácie, vytvoríme webovú stránku, ktorá umožní používateľovi vyhľadávať a zobrazovať hudobné albumy. Funkcia bude reagovať na požiadavky HTTP a ako odpoveď vráti stránku HTML. Samotný obsah stránky bude generovaný priamo vo funkcii, pričom na získanie informácií o hudobných albumoch bude naša aplikácia využívať JSON API poskytované službou iTunes.

Cieľovou platformou tejto aplikácie budú Azure Functions, pričom kód bude písaný v jazyku F#. Repozitár, ktorý obsahuje finálny kód je voľne dostupný na githube.

Vytvorenie aplikácie na Azure

Pred tým, než začneme programovať potrebujeme vytvoriť novú aplikáciu (inštanciu) typu Function App. Aplikácia pre nás predstavuje kontajner (zbierku) funkcií, ktoré spolu istým spôsobom súvisia. Toto spojenie môže byť na úrovni domény časti problému či celkovej aplikácie a vyplýva z architektúry a rozsahu projektu.

Pri vytváraní Function App máme možnosť zvoliť si hostingový plán. Pre väčšinu prípadov je najlepšou voľbou ponechať plán na základe spotreby (Consumption plan) čo znamená to, že budeme platiť podľa počtu vykonaní našich funkcií (resp. presnejšie podľa času využívania procesora).

Založenie projektu

Ak už máme portál Azure pripravený, môžeme vytvoriť nový projekt a vygenerovať v ňom našu funkciu. Na to, aby sme boli schopní tieto kroky vykonať lokálne potrebujeme mať nainštalované Azure Functions Core Tools a rozšírenie Azure Functions pre VSCode.

Spomedzi dostupných šablón si vyberieme C#, ktorý budeme následne konvertovať na F#. Keďže naša funkcia bude fungovať ako HTTP server, vytvoríme ju ako HTTP Trigger.

Konvertovanie na F# projekt

Keďže nám VSCode neponúkol možnosť vytvoriť našu funkciu v jazyku F#, musíme ju konvertovať manuálne z jazyka C#. Tento proces je jednoduchý a zmeny sú minimálne (samozrejme, okrem jazykovo špecifických rozdielov).

Po úspešnom konvertovaní bude naša funkcia vyzerať takto:

namespace com.lukasvavrek

open System
open System.IO
open System.Threading.Tasks
open Microsoft.AspNetCore.Mvc
open Microsoft.Azure.WebJobs
open Microsoft.Azure.WebJobs.Extensions.Http
open Microsoft.AspNetCore.Http
open Microsoft.Extensions.Logging
open Newtonsoft.Json

module GetPage =
    [<FunctionName("GetPage")>]
    let Run([<HttpTrigger(
                AuthorizationLevel.Function, 
                "get", 
                Route = null
            )>] req: HttpRequest,
            log: ILogger) =
        ContentResult(Content = "Hello world", ContentType = "text/html")

Komunikácia s iTunes

Aby sme naše demo spravili zaujímavejším, implementujeme komunikáciu so službou iTunes API a využijeme možnosť vyhľadávať v tejto databáze virtuálneho obsahu. Pomocou jednoduchého HTTP aplikačného rozhrania sa budeme dotazovať na hudobné albumy, na základe kľúčového slova, ktoré bude v našom prípade reprezentované menom hľadaného interpreta.

Odpoveď na naše vyhľadávanie dostaneme vo formáte JSON, ktorý spracujeme pomocou pripraveného JsonProvidera. Budeme tak môcť s odpoveďou pracovať ako s plnohodnotným F# typom.

Naša funkcia po týchto úpravách dokáže komunikovať s rozhraním iTunes a pracovať s odpoveďou vo formáte JSON:

namespace com.lukasvavrek

open System
open System.IO
open System.Threading.Tasks
open Microsoft.AspNetCore.Mvc
open Microsoft.Azure.WebJobs
open Microsoft.Azure.WebJobs.Extensions.Http
open Microsoft.AspNetCore.Http
open Microsoft.Extensions.Logging
open Newtonsoft.Json
open FSharp.Data

module GetPage =
    type ITunesSearchResult = JsonProvider<"./itunes-data.json">
    type Album = { 
        ArtistName : string
        AlbumName : string
        ArtworkUrl100 : string
        ReleaseDate : DateTimeOffset
    }

    [<FunctionName("GetPage")>]
    let Run([<HttpTrigger(
                AuthorizationLevel.Function, 
                "get", 
                Route = null
            )>] req: HttpRequest,
            log: ILogger) =
        let getAlbums musician =
            let path = "https://itunes.apple.com/search"
            let search = [
                ("term", musician);
                ("media", "music");
                ("entity", "album")
            ]

            async { 
                return! Http.AsyncRequestString(path, query=search, httpMethod="GET")
            } |> Async.RunSynchronously

        match req.Query.["name"].ToString() with
        | "" -> ContentResult(Content = "Hello world", ContentType = "text/html")
        | musician -> ContentResult(Content = (getAlbums musician), ContentType = "application/json")

Generovanie HTML stránok

V predošlom kroku sa nám podarilo získať dáta. Tie teraz využijeme pri generovaní odpovede našej funkcie, ktorú bude tvoriť text vo formáte HTML. Aby sme čo najviac oddelili dáta od prezentačnej formy, využijeme HTML šablóny.

Blob storage

Použitím HTML šablón sme zabezpečili osamostatnenie vizuálnej podoby webovej stránky. Aby sme so šablónou mohli pracovať, potrebujeme k nej našej funkcii zabezpečiť prístup. Najjednoduchším riešením je uložiť túto šablónu do premennej priamo v programe. Takýto prístup je samozrejme zlý a neposkytuje takmer žiadnu separáciu medzi vizuálom vyjadreným v HTML a kódom v jazyku F#. Lepším riešením je extrahovať HTML šablónu do samostatného súboru a načítať ju počas vykonávania funkcie. Takýto prístup je pre väčšinu jednoduchých prípadov dostatočný. Má však jednú veľkú nevýhodu. Pri zmene obsahu HTML súboru je potrebný re-deploy, teda kompletné znovu-nasadenie funkcie. Použitím tzv. Blob úložiska môžeme tieto súbory meniť dynamicky počas ostrej prevádzky našej funkcie. Kompletne tak oddelíme prácu (a proces nasadenia) front-end a back-end programátorov.

Úložisko Blobov nám ponúka jednoduchý prístup k dokumentom, organizáciu vo forme kontajnerov a automatickú podporu zo strany Azure Functions vo forme vstupných prúdov (streams).

Blob storage nám bol vytvorený spolu s našou Functions aplikáciou. Jediné čo teda potrebujeme spraviť je vytvoriť kontajner html (pre lepšiu organizáciu) a nahrať doň pripravené šablóny. Keďže chceme našu funkciu aj naďalej vyvíjať lokálne, potrebujeme do local.settings.json pridať prístupový kľúč na využívané úložisko.

Generovanie výslednej stránky

Aby bol náš kód čo najjednoduchší, využijeme služby práce so šablónami, ktoré nám poskytne knižnica Fue. Tá je navrhnutá špeciálne pre jazyk F# a ponúka široké možnosti aj pre tie najkomplikovanejšie scenáre.

Pomocou nej môžeme vykresliť zoznam albumov takto jednoducho:

<div fs-for="album in albums" class="album-container">
  <img src="{{{album.ArtworkUrl100}}}" alt="" /> 
  <p>{{{album.ArtistName}}}</p>
  <p>{{{album.AlbumName}}}</p>
</div>

Výsledný kód našej funkcie:

namespace com.lukasvavrek

open System
open System.IO
open Microsoft.Azure.WebJobs
open Microsoft.Azure.WebJobs.Extensions.Http
open Microsoft.AspNetCore.Http
open Microsoft.Extensions.Logging
open FSharp.Data
open Fue.Data
open Fue.Compiler
open System.Net.Http
open System.Net
open System.Text

module GetPage =
    type ITunesSearchResult = JsonProvider<"./itunes-data.json">
    type Album = { 
        ArtistName : string
        AlbumName : string
        ArtworkUrl100 : string
        ReleaseDate : DateTimeOffset
    }

    [<FunctionName("GetPage")>]
    let Run([<HttpTrigger(
                AuthorizationLevel.Function, 
                "get", 
                Route = null
            )>] req: HttpRequest,
            log: ILogger,
            [<Blob(
                "html/detail.html", 
                FileAccess.Read
            )>] detailTemplate: Stream,
            [<Blob(
                "html/search.html", 
                FileAccess.Read
            )>] searchTemplate: Stream) =
        let getAlbums musician =
            let path = "https://itunes.apple.com/search"
            let search = [
                ("term", musician);
                ("media", "music");
                ("entity", "album")
            ]

            async { 
                return! Http.AsyncRequestString(path, query=search, httpMethod="GET")
            } |> Async.RunSynchronously

        let serveOkResponse content =
            let response = new HttpResponseMessage(HttpStatusCode.OK)
            response.Content <- new StringContent(content, Encoding.UTF8, "text/html")
            response

        let serveSearchTemplate =
            use reader = new StreamReader(searchTemplate)
            let template = reader.ReadToEnd() 
            serveOkResponse template

        let itunesResultsToAlbum (result: ITunesSearchResult.Result) =
            { ArtistName = result.ArtistName
              AlbumName = result.CollectionName
              ArtworkUrl100 = result.ArtworkUrl100
              ReleaseDate = result.ReleaseDate }

        let serveDetailTemplate musician =
            let data = getAlbums musician

            let searchResults = ITunesSearchResult.Parse(data).Results |> Seq.map itunesResultsToAlbum
            use reader = new StreamReader(detailTemplate)
            let template = reader.ReadToEnd()
            let compiledHtml =
                init
                |> add "albums" searchResults
                |> fromText template

            serveOkResponse compiledHtml

        match req.Query.["name"].ToString() with
        | "" -> serveSearchTemplate
        | musician -> serveDetailTemplate musician

Deployment

Na záver, keď sme už konečne spokojní so stavom a funkcionalitou našej funkcie, čaká nás posledný krok a teda nasadiť ju na samotný Azure. To môžeme vykonať priamo z prostredia VSCode. Po tom, ako je proces nasadenia hotový, môžeme našu funkciu volať odkiaľkoľvek pomocou adresy URL ktorá jej bola priradená. Kvôli bezpečnosti je potrebné, aby sme funkciu volali s príslušným bezpečnostným kódom.

Záver

Serverless je moderná architektúra ktorá umožňuje programátorom zaoberať sa vývojom kódu a odbremeňuje ich od nutnosti manažovať hardvérové zdroje, operačný systém či jednotlivé služby. Pomáha nám optimalizovať naše výdavky, priamo podporuje škálovateľnosť našich aplikácií, znižuje komplexitu výsledného riešenia a skracuje čas potrebný na jeho vývoj. Ako každá iná architektúra má aj svoje nevýhody. Medzi ne patrí napríklad nutnosť viazať sa na poskytovateľa danej služby (jednotlivé platformy sú medzi sebou nekompatibilné), dlhšia odozva pre prvú požiadavku (tzv. cold start) či možná neefektivita v prípade keď máme dlhodobo bežiace úlohy. Je teda na nás aby sme zvážili všetky pre a proti a rozhodli sa správne, vzhľadom na požiadavky našich výsledných riešení.