AWS Lambda és a Spring Cloud Function

Az AWS Lambda egy AWS szolgáltatás, mely lehetővé teszi, hogy a különböző programozási nyelven megírt alkalmazásainkat (függvényeinket) anélkül tudjuk futtatni, hogy gondunk lenne az azt kiszolgáló infrastruktúrára. A szolgáltatás a serverless computing kategóriába tartozik, ami kicsit megtévesztő, hiszen itt is van futtató környezet, azonban ez számunkra láthatatlan. A másik ismert megnevezése a Function as a Service (FaaS). Természetesen más cloud szolgáltatóknak is van hasonló megoldásuk, a Microsoftnál az Azure Functions, a Google-nél a Cloud Functions.

A függvények tipikusan valamilyen beérkező eseményre reagálnak, ami lehet egy felhasználói kérés, valamilyen üzenet más szolgáltatás (pl. S3) felöl, vagy üzenet brókeren keresztül beérkező üzenet, pl. valamilyen IoT eszköz, vagy alkalmazás irányából. Miután megírjuk a választott programozási nyelven a függvényünket, azt össze kell csomagolni, és telepíteni. Ez lehet akár egy tömörített fájl, akár egy container image. A szolgáltatás rugalmassága miatt ma akár bármilyen programozási nyelven írhatunk ilyen függvényeket.

A futtatókörnyezettel nem kell foglalkozni, azt a szolgáltatás önmaga kezeli. Ez magában foglalja a hardvert és az operációs rendszert, annak frissítését, erőforrás kezelést, automatikus skálázást és naplózást. És fizetnünk is csak annyit kell, amennyit futott a függvényünk, idő alapján (ezredmásodperc alapon).

A kód futtatása természetesen konténerekben történik, melyek tartalmazzák az adott programozási nyelv futtatókörnyezetét. Azonban a szolgáltatás nem mindig indít minden bejövő eseményhez új konténert, hanem megpróbálja azokat újrahasznosítani. Ennek egyik következménye, hogy bizonyos kérések kiszolgálása tovább tart, mikor a konténert el kell indítani (ún. cold start). Bizonyos kérések kiszolgálása azonban nagyon gyorsan megtörténik (ún. warm start). A nem használt konténereket a szolgáltatás automatikusan leállítja. Másrészt mivel a kérések kiszolgálása ugyanabban a konténerben futhat, lehet ezeknek egymásra hatása (pl. ha használják a fájlrendszert).

Egy AWS Lambda konténer rendelkezésére álló memória konfigurálható 128 és 10240 MB között, természetesen egy ezredmásodperc ára is annál nagyobb, minél több memóriát használ a konténer. Valamint 512 MB és 10240 MB között tudunk hozzá tárhelyet is rendelni, minél többet, annál nagyobb felárral.

Amennyiben saját image-t állítunk elő, először a Amazon Elastic Container Registry (Amazon ECR) szolgáltatásba kell feltöltenünk.

Ha a Lambda függvényünket REST-en akarjuk meghívni, akkor még az API Gateway szolgáltatást kell használnunk.

Architektúra

Ezek alapján mikor érdemes az AWS Lambdát használni? Olyan funkciók megvalósítására ideális, amik eseményekre reagálnak, nincs rá folyamatosan szükségünk, nem kell azonnali választ adnunk, és az üzenetek beérkezése nem egyenletes. Ilyen lehet pl. a kép- és videófeldolgozás, bejövő események adatainak aggregálása, (PDF) dokumentumok generálása.

Vannak azonban esetek, amelyeknél el kell gondolkodnunk, hogy tényleg az AWS Lambda-e a jó választás. Ilyenek például a klasszikus webes alkalmazások. Itt egy gyenge forgalom esetén lehet, hogy a cold start miatt nagyobb lesz a válaszidő. Valamint egy nagyon nagy terhelésű alkalmazás esetén pedig lehet, hogy az idő alapú számlázás nem a legkedvezőbb.

A Spring Cloud Function egy olyan projekt, mely a Spring ökoszisztéma tagja, Spring Boothoz illeszkedik, és segítségével az üzleti logikát Java 8 funkcionális interfészek implementálásával tudjuk megvalósítani. Ezáltal teljesen leválasztja az infrastruktúrális elemekről. Azonban adapterekkel, konfigurációt használva össze tudjuk kapcsolni az üzleti logikát különböző szolgáltatásokkal, pl. AWS Lambdaval is. A Spring Cloud Stream használatával pedig különböző message brokerrekkel, mint pl. RabbitMQ-val vagy Apache Kafkával.

Mivel nem mindegy, hogy a function mennyi memóriát használ, és mennyi idő alatt indul el, ezért a Java bytekód nem feltétlenül a legjobb választás. Azonban a Spring Boot alkalmazásokat könnyű natívvá fordítani.

Ebben a posztban azt mutatom be, hogyan lehet egy natívra fordított, Spring Cloud Functiont használó alkalmazást AWS Lambda függvényként futtatni.

(A példa ingyenesen kipróbálható az AWS Free Tier használatával. Bankkártya adatok megadása szükséges.)

Project Reactor és a szálkezelés

A Project Reactor egy Springhez közel álló reaktív könyvtár. Erre épül a Spring Framework 5-ben megjelent WebFlux webes keretrendszer reaktív webes alkalmazások készítésére. Ez nagyban hasonlít a Spring MVC-re, azonban reaktív módon működik. Ezzel találkozhatunk akkor is, ha WebClient-et használunk REST webszolgáltatások hívására (ez a RestTemplate-et hivatott leváltani, de úgy tűnik, hogy mégis szükség van egy szinkron megvalósításra is, ezért jelent meg a 6.1-es Spring Frameworkben a RestClient). A reaktív elvekről és keretrendszerekről már írtam a Reaktív programozás posztomban.

A Reactor ún. concurrency-agnostic, ami azt jelenti, hogy nem erőltet ránk semmilyen párhuzamossági modellt. Azonban a fejlesztőnek lehetőséget ad a szálak használatára.

Ez a poszt azzal foglalkozik, hogy lehet szálakat használni, hogyan befolyásolja a szálak használatát a publishOn és subscribeOn operátor (Mono és Flux osztályokban lévő metódusok).

A REST API hypertext-driven legyen

Bár erős vitákat tapasztalok a REST körül, valahogy a hypertext-driven API-val ritkán találkozom.

Roy T. Fielding, a REST megálmodója a gyakran idézett REST APIs must be hypertext-driven cikkjében a következőt mondja:

In other words, if the engine of application state (and hence the API) is not being driven by hypertext, then it cannot be RESTful and cannot be a REST API. Period.

Erre erősít rá a Richardson Maturity Model is, mely három lépésben mutatja be a REST alapvető elemeit:

  • Level 1 - Resources
  • Level 2 - HTTP Verbs
  • Level 3 - Hypermedia Controls

És egy API csak akkor nevezhető RESTfulnak, ha az összes szinten leírtakat teljesíti, igen a hypermedia control használatát is.

De mit is jelent ez? Az állítás egyszerű: a kliensnek minden előzetes tudás nélkül igénybe kell tudnia venni a RESTful API mögötti szolgáltatásokat, és ez linkek segítségével valósulhat meg.

Pl. az előző Workflow REST API-n posztomban szereplő Issue Tracker alkalmazás a következőképp adja vissza a hibajegyek listáját:

{
  "_embedded": {
    "jtl:issueResourceList": [
      {
        "id": 1,
        "title": "Write a post about REST",
        "state": "NEW",
        "_links": {
          "self": {
            "href": "http://localhost:8080/api/issues/1"
          },
          "actions": {
            "href": "http://localhost:8080/api/issues/1/actions"
          }
        }
      }
    ]
  },
  "_links": {
    "self": {
      "href": "http://localhost:8080/api/issues"
    }
  }
}

Látható a hibajegyek listáját reprezentáló URL-jét tartalmazó self link, valamint hibajegyenként a hibajegyet reprezentáló URL-jének linkje szintén self névvel, valamint a hibajegyhez tartozó lépéseket reprenzentáló URL actions névvel.

Ez a Hypermedia as the engine of application state (HATEOAS), melyet szintén Roy T. Fielding vezetett be a disszertációjában. A HATEOAS szerint a szerver a kliens számára linkekkel jelzi, hogy mit tehet.

Az elv mögötte az, hogy a REST API-t is úgy lehessen használni, mint a webet. Ahol vannak oldalak és linkek és semmilyen előzetes információra nincs szükségem a használatához.

Hogy hogyan viszonyul ez az OpenAPI-hoz (Swaggerhez)? Elvileg a cél ugyanaz, és míg a HATEOAS a REST gonolatiságához illeszkedik, az OpenAPI inkább az RPC-hez.

Workflow REST API-n

Bár kétségtelenül a REST a legelterjedtebb kommunikációs mód, akár ugyanolyan platformon fejlesztett alkalmazások között is, REST API tervezéskor nekem sok kérdés merül fel, melyre nem kapok megnyugtató választ, és sok olyan megoldással találkozom, amelyek nem felelnek meg a REST elveknek, vagy egyszerűen csak nem tetszenek.

Sokáig próbáltam megfogalmazni, hogy mi az alapvető problémám, és talán most sikerült közelebb kerülni. Vegyünk két microservice-t, melyek mindegyike Javaban készült, Spring Boot használatával. Mindkét oldalon a programozási nyelv alapeleme az osztályok, ahol érvényesül az egységbezárás, azaz az attribútumok mellett ott vannak a metódusok, a megfelelő paraméterekkel és visszatérési értékekkel. Ez a legtöbb fejlesztőnek triviális.

A REST a kettő között viszont egy teljesen más absztrakció. Bár a REST nem mondja ki, hogy csak HTTP-vel együtt használható, más protokollal működni még nem láttam. A REST alapfogalma az erőforrás, és az azon végzett műveletek, melyeket a HTTP metódusok valósítanak meg, mint a GET, POST, PUT és DELETE. Ez nehezen feleltethető meg az objektumorientált világnak. Egy objektum létrehozására tipikusan konstruktort használunk, ebből nem feltétlen egy van. Ezen kívül egy objektumnak lehet több metódusa, amik nem feltétlenül feleltethetőek meg a HTTP metódusoknak, hiszen nem csak ezeket az alapműveleteket használjuk, valamint lehet belőle sokkal több is, mint négy. Míg a Java nyelv szabvány, addig a REST csak egy ajánlás, és az sincs megfelelően definiálva, hogy hogyan kéne implementálni, az meg aztán pláne nincs, hogy a kettőt hogyan feleltessük meg egymásnak. És ennek az az eredménye, hogy minden projekten teljesen más megoldásokat látok, a legtöbb helyen kompromisszumokkal.

Absztrakciók

Az RPC alapú kommunikáció esetén igazából nincs ilyen probléma, mert nem szükséges átfordítás, nincs szükség az absztrakció váltására. Hiszen ott is eljárások vannak, paraméterekkel és visszatérési értékekkel. A Spring Frameworkben volt is ilyen lehetőség, hogy egy távoli eljáráshívás történt, azonban a protokollt kódolás nélkül cserélni lehetett alatta, pl. választhattunk RMI-t, vagy ki emlékszik még a Hessian és Burlap protokollokra. Sajnos ez azóta eltűnt. A SOAP esetén sem volt akkora probléma az átfordítás, hiszen ott is vannak az operációk (eljárásnak felel meg), és a paraméter, valamint a visszatérési érték is egy-egy (XML) dokumentum. Vannak modernebb RPC alapú kommunikációs protokollok is, mint pl. JSON-RPC, vagy az egyre inkább elterjedő multiplatform gRPC, bináris Protocol Buffers formátummal.

Amennyiben CRUD alkalmazásról van szó, a REST használata talán triviális. De ha egy olyan alkalmazásról beszélünk, amiben komoly üzleti logika van (és hiszem, hogy a legtöbb alkalmazásnak ilyennek kéne lennie, csak a fejlesztők “butítják le” CRUD alkalmazássá, amely végső soron oda is vezethet, hogy borzasztó felhasználói felületek kerülnek kifejlesztésre), akkor már több kérdés merül fel.

Vegyünk például egy hibajegykezelő alkalmazást (issue tracker), mely rendelkezzen egy minimális üzleti logikával. Ha hibajegy kerül felvételre, az új (new) állapottal kerül létrehozásra. Amennyiben valaki elkezd rajta dolgozni, átkerül folyamatban (in_progress) állapotba. Amennyiben elkészült, átkerül megoldott (resolved) állapotba. Bár a legtöbb hibajegykezelő alkalmazás lehetővé teszi, hogy egyedi munkafolyamatot lehessen benne létrehozni, ettől most tekintsünk el, mert akkor megint más problémák merülnek fel. Ezt egy egyszerű állapotdiagrammal lehet a legegyszerűbben szemléltetni.

Absztrakciók

Talán egy hibajegy felvételét még el tudom képzelni REST API-n (bár már ott is vannak kérdéseim), azonban a különböző munkafolyamat lépések implementálásakor már bizonytalanabb vagyok. Ez a poszt ezt a problémakört járja körbe, forráskódokkal alátámasztva.

Hőmérséklet monitorozása

Nagyon régóta érdekel az IoT (Internet of Things, azaz Internetre köthető kütyük) világa, ugyanis ezek azok az eszközök, melyek összekötik a virtuális világót a való világgal. Már régóta rendelkezem egy Raspberry PI számítógéppel, melyet azóta is lelkesen használok egy alacsony fogyasztású home serverként. Telepítve van rá a Prometheus és Grafana, mely a DevOps világban egy kvázi standard monitorozó eszköz. Mindkettő ingyenesen használható, nyílt forráskódú eszköz. A Prometheus különösen alkalmas idősorok hatékony tárolására, gyors lekérdezésére, aggregált műveletek végrehajtására. Az idősorok olyan megfigyelések, melyeket egymást követő időpontokban (időszakokban) regisztrálták, és ez az időbeliség az adatok fontos tulajdonsága. Ilyen például egy szerverrel kapcsolatban a bizonyos időpontokban lekérdezett CPU és memóriahasználat, vagy a háttértárakon lévő szabad hely mérete. A Grafana használatával ezeket tudjuk vizualizálni, gyönyörű dashboardokat létrehozni és akár riasztásokat beállítani.

De melyik is lehet az az IoT eszköz, melyet a legegyszerűbben, lehetőleg barkácsolás nélkül és olcsón lehetne bekötni ebbe a rendszerbe? Már régóta szemezem a Xiaomi Mi Temperature and Humidity Monitor 2 hőmérséklet-, és páratartalom mérővel, amihez most 2000 Ft-ért lehet hozzájutni az mStore akciója keretében.

Xiaomi Mi Temperature and Humidity Monitor 2

Ez pontosan a LYWSD03MMC modell, mely egy precíz Sensirion szenzorral, 1,5”-os LCD kijelzővel rendelkezik, és Bluetooth 4.2 BLE vezeték nélküli kapcsolaton keresztül kommunikál. Egy CR2032 elemmel működik, ezt külön szerezzük be, mert nem része a csomagnak. Ezzel akár fél-egy évig képes üzemelni. Természetesen Bluetooth-on tud kapcsolódni mobiltelefonhoz, illetve Bluetooth Gateway-hez, azonban én direktbe, egy Linuxos számítógéppel, jelen esetben egy Raspberry PI-vel szeretnék hozzá kapcsolódni, és kinyerni belőle az adatokat. (Nincs szükség egyedi firmware telepítésére.)

Ezt utána csak be kell kötni a Prometheusba, mely időközönként lekérdezi és eltárolja az adatokat, majd egy Grafana dashboardot létrehozni, mely megjeleníti azokat.

Grafana dashboard

Közben természetesen szerettem volna megismerni a kapcsolódó technológiákat is.