artykułyserverless

Obsługa błędów w AWS Lambda

Brak komentarzy

AWS Lambda bez wątpienia cieszy się coraz większą popularnością. Nie brakuje materiałów, które dotyczą stworzenia przykładowej funkcji czy nawet potencjalnych przypadków użycia. Często pomijanym aspektem jest jednak obsługa błędów w AWS Lambda. Dlatego z artykułu dowiesz się:

  • Jak wygląda komunikacja z serwisem AWS Lambda?
  • W jaki sposób zwracane są obsłużone i nieobsłużone błędy z serwisu AWS Lambda?
  • Jak można obsługiwać oba typy błędów w miejscu wywołania?

Do odpowiedzi na te pytania będzie potrzebna funkcja Lambda – na początek w bardzo prostej postaci. Każde wywołanie tej funkcji zwróci ten sam model, zawierający w odpowiedzi ciąg znaków. W artykule nie będę się skupiać na samym procesie tworzenia i wdrażania funkcji. We wszystkich przykładach skorzystam z języka C# oraz .NET Core 2.1, aczkolwiek nic nie stoi na przeszkodzie, żeby użyć dowolnego języka i środowiska uruchomieniowego wspieranego przez AWS Lambda.

Kod funkcji wygląda następująco:

public class SuccessLambda
{
    public dynamic Invoke() => new { Response = "AWS Lambda function invoked correctly!" };
}

Sposoby wywołań funkcji AWS Lambda

Stworzoną funkcję możemy wywołać na kilka sposobów. Pokażę trzy z nich, a następnie przeanalizuję otrzymane wyniki w różnych przypadkach.

Konsola AWS

Podstawowym sposobem na wywołanie funkcji Lambda jest skorzystanie z dedykowanego widoku w portalu AWS. Ta metoda jest często wykorzystywana w różnego rodzaju poradnikach o tworzeniu funkcji Lambda, jednak w rzeczywistych środowiskach jest używana stosunkowo rzadko. W celu wywołania funkcji w ten sposób należy wejść do strony serwisu Lambda w portalu AWS i wyszukać funkcję, którą chcemy wywołać. Po wejściu na stronę konkretnej funkcji, trzeba odszukać przycisk Test, który umożliwia bezpośrednie wywołanie funkcji:

Wywołanie funkcji Lambda z poziomu konsoli AWS
Wywołanie funkcji Lambda z poziomu konsoli AWS

Po naciśnięciu przycisku należy jeszcze odpowiednio skonfigurować wywołanie, wprowadzając nazwę oraz ciało wysłanego żądania. Nazwa jest tylko etykietą konkretnej konfiguracji, natomiast ciało w tym przypadku nie ma znaczenia – nasza funkcja nie przyjmuje żadnych parametrów. Ważne jest natomiast, żeby był to poprawny plik JSON:

Konfiguracja wywołania funkcji Lambda
Konfiguracja wywołania funkcji Lambda

AWS CLI

Kolejny sposobem wywołania funkcji jest skorzystanie z dedykowanego narzędzia AWS CLI, które pozwala na korzystanie z API AWS z poziomu powłoki systemowej. Ta metoda jest stosowana zdecydowanie częściej niż bezpośrednie wywołanie z konsoli AWS. Może być szeroko wykorzystywana w różnego rodzaju skryptach, jak również z poziomu różnych innych usług AWS. W celu wywołania stworzonej funkcji trzeba posłużyć się następującym poleceniem:

aws lambda invoke `
  --function-name aws-lambda-error-handling-SuccessLambda-<your-unique-key> `
  response.json

W powyższej komendzie należy uzupełnić nazwę funkcji o unikalny klucz wygenerowany podczas wdrożenia. Oprócz nazwy funkcji trzeba podać również ścieżkę pliku, do którego będzie zapisana odpowiedź z funkcji.

AWS SDK

Ostatnim analizowanym sposobem jest wywołanie funkcji Lambda z naszego kodu przy pomocy AWS SDK. Takie narzędzia programistyczne dostępne są dla wielu języków programowania. Z racji tego, że w pokazanym przykładzie korzystam z C# oraz .NET Core, wywołanie funkcji również przedstawię w oparciu o te technologie. Nic nie stoi jednak na przeszkodzie, żeby funkcje były napisane w jednym języku, a wywołania w innym.

Żeby móc wywołać funkcję z poziomu kodu w C#, musimy zainstalować odpowiedni pakiet NuGet. Wtedy mamy do dyspozycji klasę AmazonLambdaClient, która może służyć między innymi do wywoływania funkcji. Kod wywołujący stworzoną funkcję może wyglądać w ten sposób:

public class AwsLambdaRunner
{
    private readonly IAmazonLambda _lambdaClient = new AmazonLambdaClient();

    public async Task<InvokeResponse> InvokeSuccessLambdaAsync()
    {
        const string successLambdaName = "aws-lambda-error-handling-SuccessLambda-<your-unique-key>";
        var invokeRequest = new InvokeRequest { FunctionName = successLambdaName };

        return await _lambdaClient.InvokeAsync(invokeRequest);
    }
}

W powyższym poleceniu należy również uzupełnić nazwę funkcji o unikalny klucz wygenerowany podczas wdrożenia. Wywołując za pomocą AWS SDK, wszystkie informacje na temat odpowiedzi na nasze zapytanie otrzymujemy w obiekcie InvokeResponse. Jest on zwracany przez metodę InvokeAsync.

Wynik udanego wywołania

W domyślnej konfiguracji, posiadając odpowiednie uprawnienia, otrzymamy zawsze pozytywną odpowiedź na żądanie.

Konsola AWS

W przypadku konsoli AWS otrzymujemy odpowiedź bezpośrednio w tym samym widoku, na którym była wywoływana funkcja:

Pozytywny wynik wywołania funkcji Lambda z konsoli AWS
Pozytywny wynik wywołania funkcji Lambda z konsoli AWS

Na skutek wywołania otrzymujemy szereg informacji: od zwróconej odpowiedzi, przez różne metryki na temat konkretnego wywołania, po pełny dziennik zdarzeń w trakcie wywołania.

AWS CLI

Wywołując za pomocą AWS CLI, w wyniku dostajemy przede wszystkim informację o rezultacie samego wywołania. Odpowiedź z narzędzia informuje o tym, czy wywołanie zostało poprawnie obsłużone i jaka wersja funkcji została wywołana:

{
    "StatusCode": 200,
    "ExecutedVersion": "$LATEST"
}

Poza tym wszystko, co zostanie zwrócone z funkcji Lambda, zostanie zapisane w ścieżce podanej podczas wywoływania – w tym przypadku response.json, w tym samym katalogu:

{"Response":"AWS Lambda function invoked correctly!"}

AWS SDK

Wywołanie przez SDK daje w odpowiedzi obiekt klasy InvokeResponse. Zawiera on różne informacje – między innymi StatusCode oraz ExecutedVersion, które mogliśmy zobaczyć również w wywołaniu przez AWS CLI:

 Obiekt InvokeResponse otrzymany po wywołaniu funkcji za pomocą AWS SDK
Obiekt InvokeResponse otrzymany po wywołaniu funkcji za pomocą AWS SDK

Jedna z właściwości zwróconego obiektu – Payload, zawiera odpowiedź z funkcji Lambda. W celu jej odczytania możemy stworzyć odpowiednią klasę po stronie wywołania i zdeserializować do niej odpowiedź:

[JsonObject]
public class SuccessLambdaResponseModel
{
    public string Response { get; }

    public SuccessLambdaResponseModel(string response)
    {
        Response = response;
    }
}
Odpowiedź z funkcji Lambda w postaci obiektu
Odpowiedź z funkcji Lambda w postaci obiektu

Wynik nieudanego wywołania

Poprzednie przykłady były dość uproszczone. W rzeczywistych zastosowaniach kod funkcji nie zawsze wykonuje się poprawnie. Mają na to wpływ różne czynniki: błędy w kodzie, niedostępność innych usług, z których korzysta funkcja, a także błędy w samym wywołaniu funkcji.

Błąd w trakcie wywołania funkcji

Pierwszym rodzajem błędów są błędy spowodowane nie tyle wadliwym działaniem funkcji, lecz problemami z jej wywołaniem. Można do nich zaliczyć na przykład brak uprawnień lub zbyt dużą liczbę jednoczesnych wywołań (zazwyczaj sterowanych parametrem Reserved concurrency). Na potrzeby analizy obsługi tego rodzaju błędów zasymuluję problem związany właśnie ze zbyt dużą liczbą jednoczesnych wywołań funkcji.

Konsola AWS

Wywołanie funkcji z tego rodzaju błędem wygląda w konsoli AWS w taki sposób:

Błędny wynik wywołania funkcji z powodu przekroczenia maksymalnej liczby jednoczesnych wywołań
Błędny wynik wywołania funkcji z powodu przekroczenia maksymalnej liczby jednoczesnych wywołań

Jak widać, kolor okna jasno wskazuje, że wywołanie funkcji nie zakończyło się sukcesem. Ponadto komunikat informuje o przyczynie tej sytuacji.

AWS CLI

W momencie wywołania tej samej funkcji przez AWS CLI otrzymamy taką odpowiedź:

An error occurred (TooManyRequestsException) when calling the Invoke operation (reached max retries: 4): Rate Exceeded.

AWS SDK

W przypadku próby wywołania takiej funkcji z poziomu AWS SDK wystąpi wyjątek, zgłoszony przez obiekt klasy AmazonLambdaClient:

Amazon.Lambda.Model.TooManyRequestsException: Rate Exceeded.

Co ciekawe, zarówno AWS CLI, jak i AWS SDK wykonają za nas ponowienia wywołania po otrzymaniu takiego rodzaju błędu.

Błąd w trakcie działania funkcji

Funkcje częściej będą kończyły się błędem z powodu jakiegoś problemu w trakcie ich działania. Tym razem również posłużę się odpowiednio przygotowaną do tego celu funkcją:

public class FailLambda
{
    public void Invoke() => throw new Exception("Exception thrown during AWS Lambda function runtime.");
}

W tym przypadku funkcja zawsze kończy się wyjątkiem w trakcie wykonywania. Znowu warto przeanalizować sposób, w jaki wynik jest przedstawiony – w zależności od sposobu wywołania funkcji.

Konsola AWS

Wywołanie z konsoli AWS przyniesie następujący rezultat:

 Błędny wynik wywołania funkcji z powodu problemu w czasie wykonywania funkcji w konsoli AWS
Błędny wynik wywołania funkcji z powodu problemu w czasie wykonywania funkcji w konsoli AWS

W interfejsie użytkownika można łatwo rozpoznać, że wywołanie zakończyło się niepowodzeniem. Okno z rezultatem jest czerwone. W polu, gdzie normalnie otrzymalibyśmy odpowiedź z funkcji, dostępne są szczegółowe informacje na temat błędu: jego typ, wiadomość, jak również stos wywołań.

AWS CLI

W tym scenariuszu otrzymamy następującą odpowiedź, wywołując funkcję przez AWS CLI:

{
    "StatusCode": 200,
    "FunctionError": "Unhandled",
    "ExecutedVersion": "$LATEST"
}

Warto zauważyć, że serwis AWS Lambda zwrócił odpowiedź z kodem 200, czyli tak jak w przypadku poprawnego wywołania. Jest to jedna z rzeczy możliwych do przeoczenia. Łatwo zrobić w tym miejscu niewłaściwe założenie, że w momencie wystąpienia błędu w naszej funkcji otrzymamy w odpowiedzi kod HTTP wskazujący błąd klienta lub serwera. Bezpośredni wynik tego zapytania nie dotyczy tego, czy funkcja wykonała się poprawnie czy nie. Dotyczy natomiast tego, czy wywołanie serwisu AWS Lambda zakończyło się sukcesem. Informacje o tym, czy sama funkcja zakończyła się pomyślnie, znajdziemy w dwóch miejscach otrzymanej odpowiedzi: polu FunctionError w bezpośredniej odpowiedzi oraz w pliku response.json, którego ścieżkę podałem podczas wywołania funkcji. Kiedy wywołanie Lambdy zakończy się błędem, w polu FunctionError zobaczymy wartość Unhandled, a w pliku response.json szczegółowe informacje na temat błędu: jego typ, wiadomość oraz stos wywołań:

{
  "errorType": "Exception",
  "errorMessage": "Exception thrown during AWS Lambda function runtime.",
  "stackTrace": [
    "at AwsLambdaErrorHandling.Functions.FailLambda.Invoke() in C:\\<code-path>\\AwsLambdaErrorHandling.Functions\\FailLambda.cs:line 7"
  ]
}

AWS SDK

W przypadku wywołania funkcji z poziomu kodu zobaczymy rezultat bardzo podobny do tego z wywołania przez AWS CLI. Obiekt klasy InvokeResponse, który zostanie zwrócony przez serwis, wygląda następująco:

Obiekt InvokeResponse otrzymany po wywołaniu funkcji za pomocą AWS SDK
Obiekt InvokeResponse otrzymany po wywołaniu funkcji za pomocą AWS SDK

W tym przypadku pole FunctionError ma wartość Unhandled, natomiast dodatkowe informacje znajdują się w polu Payload – w postaci strumienia, który po odczytaniu przyjmuje postać znaną z poprzedniego przykładu:

{
  "errorType": "Exception",
  "errorMessage": "Exception thrown during AWS Lambda function runtime.",
  "stackTrace": [
    "at AwsLambdaErrorHandling.Functions.FailLambda.Invoke() in C:\\<code-path>\\AwsLambdaErrorHandling.Functions\\FailLambda.cs:line 7"
  ]
}

Podsumowanie możliwych przypadków

Z technicznego punktu widzenia pod interfejsami użytkownika, jakimi są konsola AWS, AWS CLI oraz AWS SDK, kryje się to samo wywołanie usługi AWS Lambda. Po analizie różnych przypadków możemy wyróżnić następujące scenariusze:

Możliwe scenariusze wywołania serwisu AWS Lambda
Możliwe scenariusze wywołania serwisu AWS Lambda
  1. Udane wywołanie funkcji. Funkcja wykonała się poprawnie. W odpowiedzi otrzymujemy kod odpowiedzi 200 oraz wynik funkcji w ciele odpowiedzi.
  2. Błąd w trakcie wywołania funkcji. Wystąpił problem z wywołaniem serwisu AWS Lambda. W odpowiedzi otrzymujemy kod z klasy kodów błędu klienta lub serwera. Może to być następstwem na przykład przekroczenia limitu wywołań lub brakiem uprawnień.
  3. Błąd w trakcie działania funkcji. Wystąpił wyjątek w trakcie wykonywania kodu funkcji. W odpowiedzi otrzymujemy kod odpowiedzi 200, nagłówek X-Amz-Function-Error z wartością Unhandled, jak również szczegóły błędu w ciele odpowiedzi.

Obsługa błędnych wywołań

W trakcie wywoływania funkcji warto mieć na uwadze wyliczone scenariusze i tak skonstruować kod wywołujący, żeby pokryć wszystkie przypadki. Dobrze jest rozważyć stworzenie klienta wielokrotnego użytku, zwłaszcza kiedy w naszym rozwiązaniu wykorzystujemy wywołania funkcji Lambda w wielu miejscach. Takie podejście pozwoli na redukcję powtórzeń kodu i wprowadzi standaryzację w obrębie naszego projektu. Gdy mamy do czynienia z wieloma projektami wewnątrz organizacji, które wywołują funkcje, można pokusić się o stworzenie dedykowanego pakietu NuGet.

Tworzenie takiego klienta dobrze jest zacząć od prostego wywołania, które po prostu ukrywa przed wywołującym szczegóły implementacyjne. Wymaga to użycia interfejsu IAmazonLambda:

public class LambdaClient : ILambdaClient
{
    private readonly IAmazonLambda _amazonLambda;
    private readonly IJsonSerializer _jsonSerializer;

    public LambdaClient(IAmazonLambda amazonLambda, IJsonSerializer jsonSerializer)
    {
        _amazonLambda = amazonLambda;
        _jsonSerializer = jsonSerializer;
    }

    public async Task<TResponse> InvokeAsync<TRequest, TResponse>(string functionName, TRequest request)
    {
        var invokeRequest = new InvokeRequest
        {
            FunctionName = functionName,
            Payload = _jsonSerializer.Serialize(request)
        };

        var lambdaResponse = await _amazonLambda.InvokeAsync(invokeRequest);
        return await _jsonSerializer.DeserializeAsync<TResponse>(lambdaResponse.Payload);
    }
}

Klient posiada dwie zależności. Jedna związana jest z fizycznym wywołaniem funkcji (zależność pochodzi z pakietu Amazon.Lambda), druga natomiast z serializacją i deserializacją modeli wykorzystywanych w wywołaniach. LambdaClient implementuje interfejs ILambdaClient, który zdefiniowany jest w ten sposób:

public interface ILambdaClient
{
    Task<TResponse> InvokeAsync<TRequest, TResponse>(string functionName, TRequest request);
}

Warto stworzyć taki interfejs na wypadek, gdyby zaszła potrzeba mockowania użycia stworzonego klienta.

Obsługa błędów w AWS Lambda trakcie wywołania funkcji

Pierwszy rodzaj błędu, związany z wywołaniem funkcji, jest obsłużony przez dostarczonego przez AWS klienta w obrębie SDK, czyli AmazonLambdaClient. W momencie otrzymania innego kodu odpowiedzi niż 200, klient zgłasza jeden z wyjątków. Każdy z wyjątków jest rozszerzeniem bazowego typu, mianowicie AmazonLambdaException. Z punktu widzenia obsługi błędów mogłoby to być wystarczające rozwiązanie. Warto jednak rozważyć stworzenie swojego typu wyjątku, żeby nie zależeć od wyjątków zaimplementowanych w zewnętrznej bibliotece:

public class LambdaInvocationException : Exception
{
    private const string ExceptionMessage = "Lambda call failed during invocation.";

    public LambdaInvocationException(Exception innerException) : base(ExceptionMessage, innerException) {}
}

Kod naszego klienta możemy teraz wzbogacić o obsługę tego typu błędu, wprowadzając proste usprawnienie:

private async Task<InvokeResponse> InvokeLambda(InvokeRequest invokeRequest)
{
    try
    {
        return await _amazonLambda.InvokeAsync(invokeRequest);
    }
    catch (AmazonLambdaException e)
    {
        throw new LambdaInvocationException(e);
    }
}

Obsługa błędów w AWS Lambda w trakcie działania funkcji

Wiedząc, jakiej odpowiedzi możemy oczekiwać w przypadku błędu w trakcie działania funkcji, możemy w łatwy sposób zweryfikować, czy odpowiedź informuje o poprawnym wykonaniu lub o błędzie. W większości przypadków rozsądnie jest stworzyć kolejny dedykowany wyjątek spodziewany w tym scenariuszu:

public class LambdaRuntimeException : Exception
{
    private const string ExceptionMessage = "Lambda call failed in runtime.";

    public LambdaRuntimeException() : base(ExceptionMessage) {}
}

Po stronie klienta weryfikacja odpowiedzi może się sprowadzić do sprawdzenia odpowiedniego nagłówka:

if (!String.IsNullOrWhiteSpace(lambdaResponse.FunctionError))
{
    throw new LambdaRuntimeException();
}

Pamiętajmy, że oprócz informacji o samym fakcie wystąpienia błędu, otrzymujemy również szczegółowe informacje na temat problemu. Warto przygotować odpowiedni model i w momencie wystąpienia takiego przypadku, odczytać je i przekazać do wyjątku po naszej stronie:

public class LambdaRuntimeErrorModel
{
    public string ErrorType { get; set; }
    public string ErrorMessage { get; set; }
    public string[] StackTrace { get; set; }
}
if (!String.IsNullOrWhiteSpace(lambdaResponse.FunctionError))
{
    var runtimeError = await _jsonSerializer.DeserializeAsync<LambdaRuntimeErrorModel>(lambdaResponse.Payload);
    throw new LambdaRuntimeException(runtimeError);
}

W efekcie zastosowanych usprawnień kod stworzonego klienta wygląda następująco:

public class LambdaClient : ILambdaClient
{
    private readonly IAmazonLambda _amazonLambda;
    private readonly IJsonSerializer _jsonSerializer;

    public LambdaClient(IAmazonLambda amazonLambda, IJsonSerializer jsonSerializer)
    {
        _amazonLambda = amazonLambda;
        _jsonSerializer = jsonSerializer;
    }

    public async Task<TResponse> InvokeAsync<TRequest, TResponse>(string functionName, TRequest request)
    {
        var invokeRequest = new InvokeRequest
        {
            FunctionName = functionName,
            Payload = _jsonSerializer.Serialize(request)
        };

        var lambdaResponse = await InvokeLambda(invokeRequest);

        if (!String.IsNullOrWhiteSpace(lambdaResponse.FunctionError))
        {
            var runtimeError = await _jsonSerializer.DeserializeAsync<LambdaRuntimeErrorModel>(lambdaResponse.Payload);
            throw new LambdaRuntimeException(runtimeError);
        }

        return await _jsonSerializer.DeserializeAsync<TResponse>(lambdaResponse.Payload);
    }

    private async Task<InvokeResponse> InvokeLambda(InvokeRequest invokeRequest)
    {
        try
        {
            return await _amazonLambda.InvokeAsync(invokeRequest);
        }
        catch (AmazonLambdaException e)
        {
            throw new LambdaInvocationException(e);
        }
    }
}

Jak u Ciebie działa obsługa błędów w AWS Lambda? Podziel się tym z nami w komentarzu!

Wszystkie przykłady gotowe do samodzielnego przetestowania możesz znaleźć w naszym repozytorium.

Tags: , ,

Powiązane artykuły

Dodaj komentarz

Twój adres e-mail nie zostanie opublikowany. Wymagane pola są oznaczone *

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

Menu