Paweł Łukasiewicz
2022-06-15
Paweł Łukasiewicz
2022-06-15
Udostępnij Udostępnij Kontakt
Wprowadzenie

Powoli zbliżamy się do końca tego cyklu. Tym razem postaramy się wykorzystać wiedzę zdobytą w poprzednich wpisach w celu połączenia większej ilości usług - również pod względem praktycznym.

Główny zamysł to rozdzielenie warstw aplikacji, wykorzystanie różnych technologii oraz oparcie architektury na rozwiązaniu AWS. Słowo "architektura" brzmi może nieco na wyrost ale musimy od czegoś zacząć, żeby potem każdy mógł iść swoją drogą rozwijając własne pomysły.

Front-end przygotujemy w oparciu o Angulara. Jeżeli chcecie poczytać więcej na temat tego frameworka odsyłam do całego cyklu wpisów: Angular - wprowadzenie: część I

Z drugiej strony jeżeli posługujecie się Reactem - nie będzie żadnego problemu. Integracja będzie odbywała się przez bramkę API połączoną z funkcją Lambda, która pobierze dane ze zdarzenia i doda je do tabeli DynamoDB. Na sam koniec dodamy jeszcze jedną funkcję reagującą na nowe zdarzenia na tabeli DynamoDB w celu wysłania powiadomienia na telefon. Wpis ten podzielę na dwie części. W pierwszej skupimy się na całej infrastrukturze oraz kodzie funkcji. W drugiej części przygotujemy prosty UI, dokonamy integracji i testów.

DynamoDB

Proces dodawania tabeli został omówiony w tym wpisie: AWS - DynamoDB

Z naszej perspektywy najistotniejszy jest fakt, że DynamoDB jest bazą danych bez schematu, która wymaga jedynie nazwy tabeli i klucza głównego. W przypadku tego wpisu utworzę prosty formularz pozwalający na zamówienie nowego samochodu prosto z salonu...

Konfiguracja tabeli ograniczy się do nazwy oraz klucza będącego numerem zamówienia: AWS - integracja usług: DynamoDB

Tworzenie roli

Zanim przejdziemy do utworzenia funkcji, która doda dane do tabeli DynamoDB musimy zastanowić się nad komunikacją pomiędzy usługami - jest to krok niezbędny do utworzenia roli z odpowiednimi uprawnieniami. W naszym wypadku będą potrzebne poniższe role:

  • AWSLambda_FulLAccess;
  • AmazonDynamoDBFullAccess;
  • AmazonAPIGatewayInvokeFullAccess;
  • CloudWatchFullAccess.
AWS - integracja usług: tworzenie roli dla funkcji Lambda

Skoro rola jest już gotowa możemy utworzyć nową funkcję - pamiętajcie o przypisaniu tej roli w momencie konfiguracji Lambdy: AWS - integracja usług: rola funkcji Lambda

Lambda dodająca dane do tabeli

Pierwsza funkcja, którą utworzymy w ramach integracji będzie działała nieco inaczej…Co mam na myśli? Do tej pory nasza Lambda zwykle reagowała na zdarzenia z innych usług, tj. dodanie pliku do S3 czy wstawienie danych do tabeli DynamoDB. Tym razem to my musimy wstawić te dane do odpowiedniej tabeli. Spokojnie, wykorzystamy do tego celu AWS SDK - dodatkowo Visual Studio jest zintegrowane z naszym kontem AWS więc jedyne co będzie nam potrzebne to nazwa tabeli (oraz oczywiście dane wejściowe). Spójrzcie na poniższy kod z komentarzami, który jest gotowy do publikacji i przetestowania:

using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using Amazon.DynamoDBv2;
using Amazon.DynamoDBv2.Model;
using Amazon.Lambda.Core;
using AWSLambda5.Models;

// Assembly attribute to enable the Lambda function's JSON input to be converted into a .NET class.
[assembly: LambdaSerializer(typeof(Amazon.Lambda.Serialization.SystemTextJson.DefaultLambdaJsonSerializer))]

namespace AWSLambda5
{
    public class Function
    {
   	 /// <summary>
   	 /// A simple function that takes a string and does a ToUpper
   	 /// </summary>
   	 /// <param name="input"></param>
   	 /// <param name="context"></param>
   	 /// <returns></returns>
   	 public async Task<Carshowroom> FunctionHandler(Carshowroom input, ILambdaContext context)
   	 {
   		 try
   		 {
   			 using (var client = new AmazonDynamoDBClient())
   			 {
   				 // Generujemy jednoznaczny identyfikator naszego rekordu
   				 string orderId = Guid.NewGuid().ToString();

   				 // Wymagana paczka: using Amazon.DynamoDBv2;
   				 // Korzystamy z dostępnej metody PutItemAsync
   				 await client.PutItemAsync(new PutItemRequest
   				 {
   					 // Podajemy nazwę tabeli, którą utworzyliśmy w konsoli AWS
   					 TableName = "CarShowroom",
   					 Item = new Dictionary<string, AttributeValue>
   					 {
   						 { "OrderId", new AttributeValue { S = orderId.ToString() }},
   						 { "Brand", new AttributeValue { S =  input.Brand }},
   						 { "Model", new AttributeValue { S = input.Model }},
   						 { "Color", new AttributeValue { S = input.Color }},
   						 { "Price", new AttributeValue { S = string.Format("{0} zł", input.Price) }},
   					 }
   				 });

   				 // Krok trochę na wyrost...chciałem jednak pokazać możliwość edycji danych przychodzących
   				 // Pole Price zdefiniowane zostało jako string tak, aby móc nim manipulować
   				 Dictionary<string, AttributeValue> carDetails = client.GetItemAsync(new GetItemRequest
   				 {
   					 TableName = "CarShowroom",
   					 Key = new Dictionary<string, AttributeValue>
   					 {
   						 { "OrderId", new AttributeValue { S = orderId.ToString() } }
   					 }
   				 }).Result.Item;

   				 // Ustawiamy w naszym obiekcie pola, które mogły zostać zmodyfikowane (np. Price)
   				 input.OrderId = carDetails["OrderId"].S;
   				 input.Brand = carDetails["Brand"].S;
   				 input.Model = carDetails["Model"].S;
   				 input.Color = carDetails["Color"].S;
   				 input.Price = carDetails["Price"].S;
   			 }
   		 }
   		 catch (Exception ex)
   		 {
   			 // W razie błędów chcemy mieć możliwość sprawdzenia co się stało
   			 context.Logger.Log(ex.ToString());
   		 }

   		 return input;
   	 }
    }

    // Pamiętajcie o odpowiedniej strukturze folderów Waszego projektu
    namespace AWSLambda5.Models
    {
   	 public class Carshowroom
   	 {
   		 public string OrderId { get; set; }

   		 public string Brand { get; set; }

   		 public string Model { get; set; }

   		 public string Color { get; set; }

   		 // Pole Price celowo ustawione na string...
   		 // W logice biznesowej dodamy do ceny sufix 'zł'
   		 public string Price { get; set; }
   	 }
    }
}

Zanim przejdziemy do kolejnego kroku sprawdzimy czy nasza Lambda działa poprawnie. W tym celu dodamy zdarzenie testowe, którego parametry będą pasować do modelu przygotowanego po stronie funkcji: AWS - integracja usług: lambda dodająca dane do DynamoDB Jeżeli wszystko zostało skonfigurowane poprawnie powinniście zobaczyć powiadomienie o pomyślnym teście tak jak na powyższym zrzucie ekranu. Dodatkowo możemy przejść do naszej tabeli w celu sprawdzenia czy nowy rekord został dodany: AWS - integracja usług: sprawdzenie rekordów w DynamoDB Wszystko jest w porządku - przechodzimy do kolejnego kroku.

API Gateway

DynamoDB: gotowe.

Lambda: gotowa.

API Gateway: brak. Bramka, której nam brakuje, będzie odpowiedzialna za przekazanie żądania z formularza po stronie front-endu do kodu naszej funkcji, która doda przekazane parametry do bazy danych. Tutaj oczywiście bazujemy na poprzednich dwóch wpisach, tj. AWS Lambda - API Gateway i AWS Lambda - API Gateway (dodatkowe parametry).

Naszym zadaniem jest utworzenie nowej bramki: AWS - integracja usług: tworzenie bramki API wraz z metodą POST oraz integracją z funkcją utworzoną przed chwilą: AWS - integracja usług: integracja bramki API z Lambdą

Kolejny niezwykle ważny krok to konfiguracja Integration Request z poprawną definicją mapping template (jeżeli będziecie mieli jakieś problemy w tym kroku zerknijcie do wpisu: AWS Lambda - API Gateway (dodatkowe parametry) gdzie są również odnośniki do oficjalnej dokumentacji):

{
    "brand": $input.json('$.brand'),
    "model": $input.json('$.model'),
    "color": $input.json('$.color'),
    "price": $input.json('$.price')
}

Pozostało nam jeszcze opublikować API. Jeżeli proces przebiegnie pomyślnie możemy przetestować nasze API. Handler zdarzenia naszej funkcji powinien posiadać wszystkie szczegóły, które zostały przekazane przez zdefiniowane żądanie integracyjne POST. Pomyślne wywołanie powinno dodać szczegóły zdarzenia do tabeli DynamoDB.

W przypadku metody POST test będzie wyglądał nieco inaczej niż w poprzednim wpisie. Przechodzimy do naszych zdefiniowanych bramek, wskazujemy metodę, którą chcemy przetestować a następnie klikamy widoczny po lewej stronie przycisk Test: AWS - integracja usług: testowanie metody POST Z poziomu otwartego okna przechodzimy na sam dół w celu przygotowania Request Body:

{
	"brand":"Via API Gateway",
	"model":"Should work",
	"color":"does it really matter?",
	"price":"580000"
}
A następnie klikamy przycisk Test. Po prawej stronie zobaczycie rezultat wykonania, który powinien być podsumowany statusem 200: AWS - integracja usług: odpowiedz z API

Dla pewności przechodzimy jeszcze do naszej tabeli DynamoDB: AWS - integracja usług: spradzenie danych w DynamoDB

Lambda odczytująca dane z tabeli

Tym razem nieco prostszy krok. Musimy dodać jeszcze jedną funkcję na której określimy wyzwalacz na tabelę, którą utworzyliśmy kilka kroków temu. Lambda będzie reagowała na dodanie danych do tabeli, pobierze szczegóły zdarzenia a następnie, po delikatnym sformatowaniu wiadomości, wyśle powiadomienie SMS na zdefiniowany numer telefonu. Tym razem pomijam kroki związane z dodaniem odpowiedniej roli, utworzeniem funkcji i dodaniem wyzwalacza na DynamoDB. Jeżeli napotkacie na jakieś problemy odsyłam do poprzednich wpisów w których różne przypadki opisane są szczegółowo. Spójrzcie również na poniższy kod z komentarzami:

using System;
using System.Text;

using Amazon.Lambda.Core;
using Amazon.Lambda.DynamoDBEvents;
using System.Threading.Tasks;
using Amazon.SimpleNotificationService;
using Amazon.SimpleNotificationService.Model;
using Amazon;

// Assembly attribute to enable the Lambda function's JSON input to be converted into a .NET class.
[assembly: LambdaSerializer(typeof(Amazon.Lambda.Serialization.SystemTextJson.DefaultLambdaJsonSerializer))]

namespace LambdaDynamoSendSms
{
    public class Function
	{
    	public async Task FunctionHandler(DynamoDBEvent dynamoEvent, ILambdaContext context)
    	{
        	context.Logger.LogLine($"Beginning to process {dynamoEvent.Records.Count} records...");
        	StringBuilder sb = new StringBuilder();

        	foreach (var record in dynamoEvent.Records)
        	{
            	context.Logger.LogLine($"Event ID: {record.EventID}");
            	context.Logger.LogLine($"Event Name: {record.EventName}");

            	// TODO: Dodać logikę biznesową przetwarzania obiektu
            	// W naszym przypadku pobieramy identyfikator klucza oraz jego wartość
            	foreach (var data in record.Dynamodb.NewImage)
            	{
                	context.Logger.LogLine($"Key: {data.Key}");

                	// Dynamiczne mapowanie używane w DynamoDB
                	// N jest symbolem pora numerycznego
                	// S jest symbolem pola tekstowego
                	if (data.Value.N != null)
                	{
                    	context.Logger.LogLine($"Value: {data.Value.N}");
                    	sb.AppendLine($"Klucz: {data.Key}, Wartosc: {data.Value.N}");
                	}
                	else
                	{
                    	context.Logger.LogLine($"Value: {data.Value.S}");
                    	sb.AppendLine($"Klucz: {data.Key}, Wartosc: {data.Value.S}");
                	}
            	}
        	}

        	await Task.CompletedTask;

        	// Wymagana paczka: Amazon.SimpleNotificationService
        	var snsClient = new AmazonSimpleNotificationServiceClient(RegionEndpoint.USEast1);

        	// Reqest zawiera treść wiadomości oraz numer telefonu
        	var request = new PublishRequest
        	{
            	Message = sb.ToString(),
            	PhoneNumber = "+48XXXXXXXXX"
        	};

        	try
        	{
            	var response = await snsClient.PublishAsync(request);
        	}
        	catch (Exception ex)
        	{
            	// W razie niepowodzenia logujemy stosowną informację
            	context.Logger.LogLine($"Error sending message: {ex}");
        	}

        	await Task.CompletedTask;
    	}
	}
}

W razie napotkania problemów odsyłam do wpisu: AWS Lambda - SNS z funkcją Lambda (tutaj też znajdziecie nazwę roli, której możemy użyć w tym przypadku).

W moim przypadku funkcja (po przypisaniu roli oraz ustawienieniu wyzwalacza) przyjmuje postać: AWS - integracja usług: funkcja Lambda

Ostatni krok, który wykonamy w ramach tego wpisu to wykorzystanie bramki API w celu dodania nowego rekordu do DynamoDB - działanie takie powinno również wyzwolić funkcję, która wyśle na nasz numer telefonu powiadomienie o nowym rekordzie. Spróbujcie samodzielnie wykonać powyższe kroki - efektem powinna być wiadomość SMS: AWS - integracja usług: wysyłanie wiadomości sms

Request Body wysłany do bramki API przyjął poniższą postać:

{
	"brand":"Integration Via API Gateway",
	"model":"Infrastructure test",
	"color":"sample",
	"price":"1"
}

Podsumowanie części I

W tej części wpisu wykonaliśmy wszystkie niezbędne kroki po stronie infrastruktury AWS. Upewniliśmy się również, że wszystkie komponenty działają prawidłowo. W ostatniej części wpisu dokonaliśmy pełnego testu integracyjnego, tj:

  1. Przygotowanie Request Body i wywołanie bramki API.
  2. Bramka API przetworzyła nasz request, przekazała wszystkie parametry do funkcji Lambda, która dodała rekord do tabeli w DynamoDB.
  3. Doszło do uruchomienia w tle funkcji reagującej na dodanie nowego rekordu do tabeli DynamoDB. Dokonaliśmy prostego formatowania wiadomości a następnie wykorzystując SNS wysłaliśmy wiadomość na wskazany numer telefonu.

Jesteśmy gotowi na kolejne kroki. Wykorzystując Angulara przygotujemy prosty formularz pozwalający na złożenie zamówienia na nowy samochód. Kliknięcie przycisk Zamów spowoduje przekazanie informacji do naszej bramki API a my powinniśmy dostać powiadomienie potwierdzające złożenie zamówienia na nowy samochód (...gdyby życie było takie proste).