W pierwszym wpisie z tego cyklu przybliżyliśmy sobie do czego służy i jakie problemy może rozwiązać Serverless Framework. W kolejnym artykule przeszliśmy przez setup środowiska i uruchomiliśmy w AWS funkcje typu hello-world. Czas na prawdopodobnie ostatni wpis z tej serii gdzie przeanalizujemy sobie ciekawszy i odrobinę bardziej złożony przykładowy projekt stacka zbudowanego przy pomocy Serverless Framework. Być może pokuszę się w przyszłości o uzupełnienie tej serii o artykuł, w którym pokażę migracje pokazanego tutaj projektu z AWS do GCP. Na ten moment nie ma się co oszukiwać, Serverless Framework najbardziej nadaje się do pracy z AWS. W przypadku Google Cloud jak i innych vendorów, lista zasobów jakie możemy tworzyć i zarządzać za pomocą Serverless Framework jest znacznie uboższa. Nie mniej jednak wykonanie takiego ćwiczenia mogłoby okazać się ciekawe.
Wracając do tematu odcinka. Nasz stack będzie realizował proste CRUDowe funkcjonalności. Będzie to REST-owe API posiadające możliwość zapisywania, edytowania i usuwania książek z bazy. Będziemy wykorzystywać AWS Lambda, API Gateway, DynamoDB oraz bucketu S3. Jako środowisko programistyczne wybrałem Node.js oraz wykorzystałem framework Express.js. Dzięki użyciu Express.js jako routera HTTP mamy możliwość zastsowania innego podejście do budowania aplikacji niż klasyczne z jedną Lambdą per jeden endpoint. Więcej o tym w dalszej części
Uruchomienie projektu
Zaznaczę na wstępie, że do uruchomienia projektu będzie konieczna instalacja Serverless Framework oraz konfiguracja rolia dla aws cli. Opis tego procesu znajdziesz w poprzednim artykule z serii. Kod projektu, który przygotowałam możesz pobrać z mojego repozytorium GitHub
Po zaciągnięciu źródeł jedyne co musimy zrobić zaciągnąć zależności Noda a następnie uruchomić wrzutkę do aws za pomocą Serverless Framework.
npm install
sls deploy
Na wykonanie deploymentu potrzebna jest chwila cierpliwości. Następuje obecnie proces budowania naszej paczki, a następnie utworzenie i uruchomienie szablonu CloudFormation. Szablon ten powstał w oparciu o plik konfiguracyjny Serverless Frameworka serverless.yml. Gdy proces ten dobiegnie końca nasza stack został utworzony i wykonany. Możesz podejrzeć w panelu CloudFormation jakie zasoby zostały utworzone.
Jak widzimy szablon CloudFormation stworzył dla nas żądane zasoby, tabele w DynamoDB, bucket S3, API Gateway oraz co najważniejsze Lambdę z kodem naszej funkcji.
Możesz teraz przetestować działanie programu, wywołując publicznie wystawione endpointy przez API Gateway. AWS utworzył dla Ciebie w tym celu tymczasową domenę. Możesz ją odnaleźć w zakładce samego API GW bądź w outpucie polecenia sls deploy.
Tak jak wspomniałem, kod funkcji to prosta aplikacja oparta o Express.js, która udostępnia API do zarządzania książkami. Poniżej dostępne funkcjonalności
curl -X GET 'https://<domena>/books'
curl -X GET 'https://<domena>/books/<id>'
# Zwraca wszystkie książki zapisane w DynamoDB lub książkę o danym id
curl -X POST 'https://<domena>/books' \
--header 'Content-Type: application/json' \
--data-raw '{
"bookId": "1",
"bookName": "The Hobbit",
"author": "Tolkien"
}'
# Zapisuje książkę do DynamoDB
curl -X PUT 'https://<domena>/books/<id>' \
--header 'Content-Type: application/json' \
--data-raw '{
"bookName": "The Lord of the Rings",
"author": "Tolkien"
}'
# Aktualizuję książkę o danym id
curl -X DELETE 'https://<domena>/books/<id>'
# Usuwa książkę z DynamoDB
Wskazówki do pracy z Serverless Framework
Czas wykonania deploymentu może potrwać nawet do kilku minut (w zależności od złożoności projektu). Nie musimy jednak za każdym razem gdy wprowadzamy zmiany w kodzie uruchamiać całego deploymentu. W takim przypadku potrzebne jest nam jedynie wrzucenie nowej paczki z kodem do Lambdy. Aby to wykonać wystarczy wykonać poniższe polecenie
sls deploy -f books
# books to nazwa funkcji
Jeśli zmieniamy jedynie konfiguracje Lambdy to polecenie również zda egzamin. Jeśli jednak dokonujemy zmiany w konfiguracji innych komponentów w pliku serverless.yml konieczne będzie wykonanie pełnego deploymentu, aby stack CloudFormation również zaktualizował nasze zasoby w AWS.
Jest jeszcze jedna ciekawa funkcja przyspieszająca pracę z Serverless Framework. Chodzi o możliwość uruchomienia (emulowania) naszych funkcji Lambda na lokalnym komputerze bez potrzeby wrzucania kodu do chmury. Możliwość tą mamy dzięki pluginowi serverless-offline. Wystarczy wykonać poniższe polecenie a nasze funkcje zostaną uruchomione na lokalnym emulatorze Lambdy i będą dostępne na localhost dzięki emulatorowi API Gateway. Twoja funkcja będzie miała również dostęp do innych zasobów z chmury takich jak DynamoDB. Pamiętaj jednak, że gdy uruchamiasz funkcję lokalnie korzysta ona z roli IAM podpiętej do aws cli a nie z tej, która utworzona została poprzez CloudFormation.
sls offline
Po raz kolejny przypominam abyś na zakończenie swoich prac bądź eksperymentów czyścił po sobie zasoby w chmurzę. Nie musisz jednak robić tego ręcznie. Wystarczy odpalić poniższe polecenie a nasz stack zostanie zroll-backowany i usunięty. Pamiętaj, że utworzona tutaj Lambda będzie publicznie dostępna w internecie. Nie chciałbyś w takiej sytuacji aby ktoś dokonał na twojej infrastrukturze atak typu Denial of Wallet
sls remove
Struktura pliku serverless.yml
Przyjrzyjmy się teraz bliżej krok po korku głównemu plikowi, w którym konfigurujemy nasz projekt Serverless Framework. W mojej przykładowym projekcie zawarłem jedynie część możliwej konfiguracji. Serverless Framework udostępnia znacznie większa liczbę komponentów do skonfigurowania (mowa tutaj o AWS, dla innych vendorów, lista ta jest zdecydowanie uboższa).
service: serverless-books
plugins:
- serverless-offline
custom:
tableName: books
tableKey: bookId
serverless-offline:
useChildProcesses: true
W pierwszym polu nadajemy nazwę projektu. Nazwę tę odziedziczy m.in. funkcja Lambda, rola IAM, bucket S3 naszego deploymentu. Poniżej mamy zdefiniowaną listę pluginów. U nas jest to jedynie wcześniej wspomniany serverless-offline. Tutaj znajdziesz oficjalne repozytorium innych ciekawych pluginów
Pod polem custom wyłącznie dla przykładu pokazałem, że możemy definiować parametry stałe, do których możemy odnosić się w naszych plikach konfiguracyjnych.
provider:
name: aws
runtime: nodejs12.x
region: eu-west-1
tags:
project: cloud-box.pl
tracing:
lambda: true
iamRoleStatements:
- Effect: Allow
Action:
- dynamodb:DescribeTable
- dynamodb:Query
- dynamodb:Scan
- dynamodb:GetItem
- dynamodb:PutItem
- dynamodb:UpdateItem
- dynamodb:DeleteItem
Resource: 'arn:aws:dynamodb:eu-west-1:*:*'
environment:
TABLE_NAME: ${self:custom.tableName}
BUCKET_NAME: 'serverless-books-dev-bucket-name'
W tej sekcji konfigurujemy najważniejsze informacje. Vendora chmury, środowisko deweloperskie i region. Opcjonalnie mamy możliwość dodania tagów dla zasobów oraz włączenia usługi AWS X-Ray, która pozwala na analizę i debugowanie naszej aplikacji.
Konfigurujemy tutaj również rolę IAM dla funkcji Lambda. Mamy możliwość utworzenia nowej roli jak w tym przypadku, bądź podpięcia istniejącej roli. W sekcji environment mamy możliwość zdefiniowania wartości, które później możemy przekazać bezpośrednio do kodu.
Mały dodatek w tym miejscu. Jak widzisz zdefiniowałem tutaj stałą o nazwie BUCKET_NAME. W pliku resources/s3.yml znajduje się definicja tego bucketu. Jest to bucket, który wykorzystywany jest w aplikacji. Gdy do DynamoDB dodasz książkę, a następnie do tego bucketa zapiszesz plik <id_ksiązki>.jpg to odpytując endpoint books/<id_ksiązki> w odpowiedzi dostaniesz json z dodatkowym polem url. Aplikacja wstawia w to pole wygenerowany presignedUrl dla danego obrazka z S3. Pamiętaj jednak, że nazwa Twojego bucketu będzie inna. Musisz ją tutaj podmienić !
functions:
books:
handler: functions/books.handler
memorySize: 512
timeout: 10
events:
- httpApi:
method: "*"
path: /books
- httpApi:
method: "*"
path: /books/{params}
resources:
- ${file(resources/dynamoDb.yml)}
- ${file(resources/s3.yml)}
Pod sekcją functions mamy konfigurację naszej Lambdy. To tylko podstawowa konfiguracja, w dokumentacji możesz sprawdzić więcej parametrów konfiguracyjnych.
Pod polem events definiujemy co będzie wywoływać naszą funkcję. W naszym wypadku Lambda jest wystawiona poprzez API Gateway z wykorzystaniem HTTP API. HTTP API jest z definicji prostszą, szybszą i tańszą opcją Gatewaya niż REST API. Gdybyś jednak sam chciał to zweryfikować to zostawiłem w kodzie wykomentowaną analogiczną konfigurację z REST API.
W tym miejscu winien Ci jestem wyjaśnienie odnośnie podejścia które zastosowałem w aplikacji. Jak zdążyłeś zauważyć nasza aplikacja składa się z jednej w funkcji. Jeśli miałeś do czynienia wcześniej z serverless to mogłeś usłyszeć, że pojedyncza funkcja powinna realizować jedną funkcjonalność. W naszym przypadku mamy jednak jedną funkcję, która realizuje wszystkie CRUD-owe operacje. Dzieje się tak za sprawą wykorzystania Express.js, który wewnątrz funkcji nawiguje requestami HTTP. Na podstawie metody HTTP określa, którą funkcjonalność zrealizować. Wykonałem taki zabieg aby pokazać Ci, że nie zawsze należy trzymać się zasady 1 Lambda = 1 funkcjonalność. W przypadku tak prostych operacji moim zdaniem dobrze jest umieszczać pewne funkcjonalności w obrębie pojedynczej Lambdy jeśli to możliwe (np. takie realizujące operacje w obrębie tego samego obiektu domenowego). Ma to swoje oczywiste minusy. Skaluje nam się tylko jedna Lambda oraz tylko jedną Lambdę możemy skonfigurować. Przykładowo w podejściu 1 Lambda = 1 operacja, wiedząc, że systemie będziemy mieć więcej odczytów niż zapisów, moglibyśmy przydzielić więcej pamięci dla Lambdy wywoływanej żądaniem typu GET.
Pod polem resources znalazły się odwołania do oddzielnych plików, w których również może znajdować się definicja zasobów które chcemy utworzyć. W tym przypadku jest to DynamoDb oraz S3. Równie dobrze każda Lambde moglibyśmy zdefiniować w oddzielnym pliku.
Podsumowanie
Serverless Framework to naprawdę ciekawe i przydatne narzędzie, któremu warto się przyjrzeć jeśli jesteś zainteresowany pracą z Lambdą jak i samym Serverless. Mam nadzieję, że projekt ten będzie dalej rozwijany i ulepszany. Fajnie byłoby zobaczyć w przyszłości większą liczbę opcji dla innych vendorów chmurowych. Tego sobie i wam życze