artykuły

AWS Lambda z C# i .NET Core

Brak komentarzy

AWS Lambda wspiera kilka środowisk uruchomieniowych. W obecnej chwili (maj 2020) jest ich aż 6. Ponadto istnieje możliwość implementacji swojego własnego środowiska uruchomieniowego. Jednym ze wspieranych środowisk jest .NET Core w wersjach 2.1 oraz 3.1 – czyli tych, które obecnie są jedynymi wersjami LTS. Najpopularniejszym językiem, którego można używać korzystając z tego frameworka, jest C#.

Z tego artykułu dowiesz się:

  • jakich narzędzi używać w efektywnej pracy z AWS Lambda w połączeniu z C# oraz .NET Core,
  • w jaki sposób tworzyć oraz wdrażać funkcje Lambda, które używają środowiska uruchomieniowego .NET Core,
  • jak weryfikować działanie funkcji.

Podczas pracy z narzędziami zintegrowanymi z powłoką systemową oraz pisania skryptów używam PowerShella. Nic nie stoi jednak na przeszkodzie, żeby analogiczne komendy z niewielki wysiłkiem przenieść do innego środowiska.

Narzędzia przydatne w pracy z AWS Lambda

AWS CLI

Podstawowym narzędziem w pracy z AWS jest AWS CLI. Instrukcję instalacji można znaleźć w oficjalnej dokumentacji. Po instalacji należy je skonfigurować, żeby umożliwić pracę z naszym kontem AWS.

.NET Core CLI

Pomocnym narzędziem przy tworzeniu funkcji lambdy przy użyciu platformy .NET Core jest .NET Core CLI. Zintegrowany z powłoką systemową interfejs pozwala na efektywną pracę z projektami, które są oparte o ten framework. Jedną z funkcjonalności wiersza poleceń jest tworzenie startowych projektów zgodnych z naszymi potrzebami. W tym celu wystarczy wywołać komendę:

dotnet new -all

Po jej wykonaniu otrzymasz podobny rezultat:

Lista nowych projektów dostępnych w .NET Core CLI

Pracując z AWS Lambda, możesz skorzystać z dodatkowego pakietu oferowanego przez Amazon:

dotnet new -i Amazon.Lambda.Templates

Po instalacji dostępne są nowe typy projektów, bardzo przydatne podczas rozpoczynania pracy z interesującą nas usługą:

Lista nowych projektów dostępnych w .NET Core CLI po instalacji pakietu z szablonami

Kolejnym ciekawym rozszerzeniem jest Amazon Lambda Tools. Przyda się przede wszystkim w procesie wdrażania funkcji na środowisku. W celu instalacji również wystarczy wywołać odpowiednią komendę w wierszu poleceń:

dotnet tool install -g Amazon.Lambda.Tools

AWS SAM CLI

Kolejnym przydatnym narzędziem jest AWS SAM CLI. Służy przede wszystkim do lokalnego uruchamiania naszych funkcji. Kompletną instrukcję, w jaki sposób je zainstalować, znajdziesz tutaj.

Struktura projektu

Wspomniane wcześniej szablony stanowią bardzo dobry punkt początkowy w tworzeniu funkcji, które wymagają na starcie pewnych integracji z innymi usługami AWS. W tym rozdziale opiszę, w jaki sposób własnoręcznie stworzyć prostą funkcję. Po stworzeniu nowej solucji oraz projektu typu Class library w .NET Core musisz zainstalować pakiet NuGet Amazon.Lambda.Core. Za jego pomocą będziesz w stanie stworzyć poprawną funkcję Lambda. Po tej akcji warto stworzyć nową klasę, która będzie reprezentacją funkcji. Ja wybrałem trywialny przykład, w którym moja funkcja będzie obliczała końcową ocenę studenta na podstawie przekazanych do niej ocen cząstkowych. Nie wchodząc w szczegóły implementacyjne samej logiki obliczania, funkcja wygląda w następujący sposób:

public class CalculateStudentFinalGradeLambda
{
    public CalculateStudentFinalGradeResponse Invoke(CalculateStudentFinalGradeRequest request)
    {
        var lectureGrade = new StudentGrade(request.LectureGrade);
        var exerciseGrade = new StudentGrade(request.ExerciseGrade);
        var workshopGrade = new StudentGrade(request.WorkshopGrade);
        var finalGrade = new StudentFinalGrade(lectureGrade, exerciseGrade, workshopGrade);

        return new CalculateStudentFinalGradeResponse
        {
            FinalGrade = finalGrade.Value,
            HasPassed = finalGrade.HasPassed
        };
    }
}

Drugim obowiązkowym elementem w projekcie (oprócz samej funkcji) jest ustawienie Assembly attribute. Ten atrybut umożliwi poprawną deserializację oraz serializację modeli użytych odpowiednio na wejściu i wyjściu funkcji. Możesz to zrobić bezpośrednio w klasie funkcji. Warto jednak wydzielić ten element do osobnego pliku, ponieważ wystarczy przeprowadzić to raz w obrębie biblioteki (niezależnie od liczby funkcji). Do definicji potrzebna jest implementacja interfejsu dostarczonego przez bazowy pakiet użyty do tworzenia funkcji Lambda: ILambdaSerializer. Możesz dostarczyć własną implementację, ale warto rozważyć użycie jednej z dwóch gotowych. Pierwsza z nich znajduje się w pakiecie NuGet Amazon.Lambda.Serialization.SystemTextJson. Opiera się ona na przestrzeni nazw System.Text.Json, wprowadzonej w .NET Core 3.0. Należy zainstalować wspomniany pakiet, a następnie zdefiniować atrybut w taki sposób:

using Amazon.Lambda.Serialization.SystemTextJson;

[assembly: LambdaSerializer(typeof(DefaultLambdaJsonSerializer))]

Istnieje również alternatywa opierająca się na znanej bibliotece Newtonsoft.Json. Podobnie jak w poprzednim przypadku, gotowa implementacja dostarczona jest w odpowiednim pakiecie NuGet – tym razem w Amazon.Lambda.Serialization.Json. Po instalacji pakietu możesz użyć go w atrybucie w taki sposób:

using Amazon.Lambda.Serialization.Json;

[assembly: LambdaSerializer(typeof(JsonSerializer))]

Wdrażanie funkcji

Funkcję Lambda najłatwiej wdrożyć za pomocą AWS SAM oraz CloudFormation. W tym celu musisz zdefiniować plik opisujący funkcję pod kątem infrastrukturalnym. Żeby to osiągnąć, należy stworzyć plik YAML w katalogu projektu. Wewnątrz pliku trzeba zdefiniować wszystkie potrzebne właściwości funkcji:

Transform: AWS::Serverless-2016-10-31
Description: Example of Lambda Functions in .NET Core
Resources:
  CalculateStudentGradeLambda:
    Type: AWS::Serverless::Function
    Properties:
      FunctionName: calculate-student-grade-lambda
      Handler: AwsLambdaDotnet.Functions::AwsLambdaDotnet.Functions.CalculateStudentFinalGrade.CalculateStudentFinalGradeLambda::Invoke
      Runtime: dotnetcore3.1
      CodeUri: bin/Release/netcoreapp3.1/publish
      MemorySize: 1024
      Timeout: 10

Zawartość jest dość czytelna i myślę, że większość właściwości nie wymaga komentarza. Pełną dokumentację tego zasobu znajdziesz tutaj. Warto wytłumaczyć przede wszystkim postać, w jakiej należy podać Handler funkcji. Składa się on z 3 członów:

  • nazwy projektu (czyli wynikowego pliku dll),
  • pełnej nazwy typu (wraz z przestrzenią nazw), w którym zdefiniowana jest metoda, którą chcemy wywołać,
  • metody, którą chcemy wywołać.

Posiadając plik opisujący funkcję Lambda, przystąp do wdrożenia. Bardzo pomocne będzie wcześniej wspomniane narzędzie Amazon Lambda Tools. Zakładając, że wcześniej zostało skonfigurowane konto, na które chcesz wdrożyć funkcję oraz że zostały zainstalowane wspomniane wcześniej narzędzia, wystarczy wykonać następujący skrypt w głównym katalogu solucji:

dotnet publish $PSScriptRoot/AwsLambdaDotnet.Functions -c Release
dotnet lambda deploy-serverless `
    --project-location $PSScriptRoot/AwsLambdaDotnet.Functions `
    --configuration Release `
    --region eu-west-1 `
    --stack-name aws-lambda-dotnet `
    --s3-bucket your-deploy-bucket `
    --s3-prefix AwsLambdaDotnet.Functions/ `
    --template $PSScriptRoot/AwsLambdaDotnet.Functions/serverless.yaml

Pierwsza komenda przygotowuje kod do wdrożenia, a druga – faktycznie je realizuje. W treści komendy należy zmienić parametr --s3-bucket na nazwę wcześniej stworzonego bucketa, który będzie służył do przechowywania artefaktów wdrożenia. Przy pierwszym wywołaniu skryptu powinniśmy zobaczyć rezultat publikowania projektu oraz pełny proces wdrażania funkcji:

Rezultat wdrożenia stacka CloudFormation z funkcją Lambda

Testowanie działania rozwiązania

Weryfikacja działania w chmurze

Po wdrożeniu funkcji warto zweryfikować jej działanie. Możesz zrobić to, wywołując ją między innymi bezpośrednio z portalu AWS lub z linii poleceń. W celu wywołania funkcji z portalu należy znaleźć ją na liście funkcji i wywołać z poprawnym zapytaniem. Przykładowe zapytanie wykorzystane do wywołania funkcji może wyglądać następująco:

{
    "LectureGrade": 3,
    "ExerciseGrade": 4,
    "WorkshopGrade": 4.5
}

Po wykonaniu funkcji powinniśmy otrzymać komunikat z odpowiedzią:

Wynik poprawnego wywołania funkcji

Można również wywołać funkcję z wiersza poleceń za pomocą komendy:

aws lambda invoke `
  --function-name calculate-student-grade-lambda `
  --payload file://payload.json `
  response.json

Plik payload.json powinien zawierać wcześniej wspomniane zapytanie, natomiast plik response.json będzie zawierać odpowiedź po wywołaniu funkcji. Po wykonaniu komendy w wierszu poleceń powinna pojawić się informacja o poprawnym wywołaniu funkcji. Z kolei plik response.json powinien zawierać tę samą odpowiedź co w portalu.

Lokalne uruchamianie funkcji

W celu przetestowania działania funkcji niekoniecznie trzeba wdrażać ją na środowisko. Dzięki AWS SAM CLI możesz uruchomić funkcję lokalnie. Żeby to zrobić, najpierw stwórz środowisko symulujące rzeczywiste środowisko wywołania funkcji. W głównym katalogu solucji należy wywołać następującą komendę:

sam local start-lambda -t AwsLambdaDotnet.Functions/serverless.yaml

To spowoduje przygotowanie kontenera gotowego na obsługę wywołań. Po tej operacji możesz wywołać funkcję następującym poleceniem:

aws lambda invoke `
    --function-name calculate-student-grade-lambda `
    --payload file://payload.json `
    --endpoint-url "http://127.0.0.1:3001" `
    response-local.json

Podobnie jak w przypadku wywoływania funkcji w rzeczywistym środowisku, w odpowiedzi otrzymamy informację o sukcesie, a odpowiedź zostanie zapisana we wskazanym przez nas pliku.

Testy automatyczne

W każdym oprogramowaniu testy automatyczne stanowią bardzo istotny element. Nie inaczej jest w przypadku tworzenia funkcji Lambda. Powinniśmy dążyć do tego, żeby jak największa część logiki w przygotowywanej funkcji była pokryta testami jednostkowymi. Warto jednak przygotować również zestaw testów automatycznych, które weryfikują poprawność działania funkcji już po wdrożeniu na środowisko chmurowe. Taki test mógłby wyglądać w następująco:

private readonly IAmazonLambda _lambdaClient = new AmazonLambdaClient();
private const string CalculateStudentFinalGradeLambdaName = "calculate-student-grade-lambda";

[Fact]
public async Task InvokeLambda_WithCorrectParameters_ShouldReturnCorrectResponse()
{
    // Given
    const double validExerciseGrade = 3.5;
    const double validLectureGrade = 2.1;
    const double validWorkshopGrade = 4.9;

    var requestModel = new CalculateStudentFinalGradeRequest
    {
        ExerciseGrade = validExerciseGrade,
        LectureGrade = validLectureGrade,
        WorkshopGrade = validWorkshopGrade
    };

    var lambdaRequest = new InvokeRequest
    {
        FunctionName = CalculateStudentFinalGradeLambdaName,
        Payload = JsonSerializer.Serialize(requestModel)
    };

    // When
    var lambdaResponse = await _lambdaClient.InvokeAsync(lambdaRequest);

    // Then
    var responseModel = await JsonSerializer.DeserializeAsync<CalculateStudentFinalGradeResponse>(lambdaResponse.Payload);
    responseModel.Should().NotBeNull();
    responseModel.FinalGrade.Should().NotBe(default);
    responseModel.HasPassed.Should().BeTrue();
}

Test stworzony w ten sposób możesz z powodzeniem dołączyć do swojego procesu CI/CD jako weryfikację działania po wdrożeniu nowej wersji. We wszystkich przykładach zakładaliśmy, że wywołanie funkcji zakończy się sukcesem. Jak radzić sobie z obsługą błędów podczas wywołań? Tego możesz dowiedzieć się w tym artykule.

Wszystkie przykłady gotowe do samodzielnego przetestowania znajdziesz w naszym repozytorium.

Tags: , ,

Powiązane artykuły

Dodaj komentarz

Twój adres email nie zostanie opublikowany. Pola, których wypełnienie jest wymagane, są oznaczone symbolem *

Wypełnij to pole
Wypełnij to pole
Proszę wprowadzić prawidłowy adres email.
You need to agree with the terms to proceed

Menu