
AWS Step Functions API Endpoints
Znowu mamy coś nowego w AWS Step Functions. Super. Dobrze się patrzy na rozwój tej usługi. Do tej pory, aby wywołać zewnętrzne API w Step Function, trzeba było użyć właśnie Lambdy lub konfigurować własne proxy przy użyciu API Gateway. Jakiś czas temu, możliwość wykonywania requestów http dostał Event Bridge. Pisałem już o tym. Od teraz Step Function może także API wywołać sama za pomocą AWS Step Functions API Endpoints.
W tym artykule postaram się pokazać co potrafi nowy rodzaj taska, a mianowicie
W drugiej części przygotujemy za pomocą cdk rozwiązanie, które będzie z nowej możliwości korzystało.
AWS Step Functions i API Endpoints
Wykonanie requestu do endpointa API wymaga skonfigurowania kulku rzeczy. I Step Functions oczywiście taką konfigurację umożliwia. Przejdźmy, krok po kroku, przez przygotowanie takiej konfiguracji. Wykorzystamy przy tym kolejną nowość, którą AWS zaprezentowało ostatnio, a mianowicie możliwoś testowania konkretnych stanów Step Function, która ułatwia pracę w worklow studio. Można sprawdzić swoje pomysły nawet bez tworzenia całej Step Function.
To co musimy skonfigurować na pewno to namiar na endpoint, do którego będziemy wysyłali requesty, metodę, oraz sposób uwierzytelnienia. A więc po kolei.
Endpoint
Jak widzicie, możemy sobie adres endpointa zakodować na sztywno lub pobrać go z danych wejściowych. Na samą myśl o hardkodowaniu dostaję wysypki i w przykładowym rozwiązaniu skorzystamy z tej drugiej możliwości, ale w tym momencie skorzystamy ze stałego endpointa, którym będzie mała aplikacja zdeployowana za pomocą usługi AppRunner. U mnie wygląda to tak:
Po wykonaniu requestu powinniśmy dostać w odpowiedzi adres IP i nazwę hosta
Sama apka, która będzie służyła za serwer dostępna jest tutaj, a gotowy obraz dockerowy możecie pobrać stąd. Jest to prosta aplikacja, którą przygotowałem kilka lat temu na szkolenia. Nic nie stoi oczywiście na przeszkodzie, aby wykorzystać inne rozwiązanie jako serwer.
Metoda HTTP
Wybór oczywiście spory
na nasze potrzeby wystarczy GET.
Ponownie możemy wybrać metodę korzystając z payload przekazanego do taska.
Uwierzytelnienie
Tutaj sprawy nam się minimalnie komplikują. Step Functions w celu uwierzytelnienia requestu używają connections z usługi Event Bridge. Połączenia EventBridge obsługują schematy autoryzacji Basic, OAuth i API Key. Możemy je oczywiście używać w kilku taskach Step Function i w samym Event Bridge. Tutaj jest instrukcja krok po kroku jak takie połączenie skonfigurować.
W samej konfiguracji taska podajemy arn do Event Bridge Connection. Możemy oczywiście przekazać go także w danych wejściowych.
Ja mam już takie połączenie utworzone. Wykorzystam je więc tutaj
Rola – Uprawnienia
Do uruchomienia będziemy oczywiście potrzebowali roli dla naszej step function. Nie będę komplikował na tym etapie sprawy, poprosimy AWS o wygenerowanie roli za nas.
Przechodzimy do zakładki Config
Wybieramy Create new role i w sekcji Permissions powinniśmy zobaczyć jakie uprawnienia dostanie nasza funkcja.
Testujemy
Na tym etapie cała konfiguracja powinna wyglądać podobnie do mojej.
Możemy już wywołać naszego taska i go przetestować. Jak już wspomniałem użyjemy do tego nowej możliwości, czyli testowania pojedynczych stanów w Step Function. Zaznaczamy nasz krok Call third-party API i klikamy w Test state
Wyświetli nam się okno, w którym możemy przetestować nasz request
Startujemy test i po chwili powinniśmy dostać „logi” z połączenia
Działa!!!
Swoją drogą, ta możliwość przetestowania pojedynczego stanu też robi robotę. 🙂 Ale o tym być może przy innej okazji.
Dodatkowe możliwości
To nie wszystko. Możemy oczywiście dodać kolejne elementy do naszego requestu, takie jak payload, czy też nagłówki. Spróbujmy szybko zająć się nagłówkami.
Nagłówki także możemy podać bezpośrednio lub podczas wykonaniu Step Function pobrać je z danych wejściowych
Dodajmy sobie dwa nagłówki w postaci
{ "header-1": "qq1", "header-2": "qq2" }
do naszej konfiguracji
I ponownie przetestujmy połączenie.
Znowu działa. 🙂
Step Functions API endpoints – przykład cdk
Dobrze, wiemy, że działa. Wiemy co jest potrzebne do skonfigurowania takiego połączenia do API ze Step Function. Zrobimy sobie teraz prawie to samo za pomocą cdk. Prawie to samo.
Całość możecie oczywiście pobrać z repozytorium GitHub.
Dodatkowo przygotujemy sobie construct cdk z prostym API, które będzie po prostu odbierało nasze requesty. Cały stack będzie składał się więc ze Step Function, API Gateway oraz Lambdy. Komunikacja będzie wyglądała następująco
To czego będziemy potrzebowali, jeżeli chodzi o samą Step Function i task typu Call third-party API to:
- Step function
- Rolę dla step function
- API Connection
To po kolei…
Step Function
Nasza funkcja będzie wyglądała następująco.
Poza samym wywołaniem API dodamy jeden state, w którym „odpakujemy” response z serwera, które będzie w postaci Base64.
Definicja jest prosta, więc pokażę ją tutaj całą. Dostępna jest także w pliku asl.json w projekcie
{ "Comment": "A description of my state machine", "StartAt": "Call third-party API", "States": { "Call third-party API": { "Type": "Task", "Resource": "arn:aws:states:::http:invoke", "Parameters": { "ApiEndpoint.$": "$.endpoint-url", "Method": "POST", "Authentication": { "ConnectionArn.$": "$.connection-arn" }, "RequestBody.$": "$.body" }, "Retry": [ { "ErrorEquals": [ "States.ALL" ], "BackoffRate": 2, "IntervalSeconds": 1, "MaxAttempts": 3, "JitterStrategy": "FULL" } ], "Next": "Pass" }, "Pass": { "Type": "Pass", "End": true, "Parameters": { "data.$": "States.Base64Decode($.ResponseBody.data)" } } } }
Na pewno zwróciliście uwagę, że w przypadku niepowodzenia, wykonywane są 3 dodatkowe próby połączenia z API. I nic nie musimy sami programować. Mniej kodu, mniej błędów. 😉
Dane do połączenia będą przekazywane poprzez Task input w postaci:
{ "endpoint-url": "API URL", "connection-arn": "API connection Arn", "body": "payload" }
W przykładzie wykorzystujemy metodę POST, a API będzie zwracało przesłany w body payload.
Poniżej tworzymy jeszcze API Connection
def _build_api_connection(self) -> aws_events.Connection: connection = aws_events.Connection(self, "Connection", authorization=aws_events.Authorization.api_key("x-api-key", SecretValue.unsafe_plain_text('secret-value')), description="Connection with API Key x-api-key", ) return connection
rolę dla Step Function
def _build_step_function_role(self) -> aws_iam.Role: policy_document = aws_iam.PolicyDocument() states_policy_statement = aws_iam.PolicyStatement(actions=[ 'states:InvokeHTTPEndpoint', ], effect=Effect.ALLOW, resources=['*']) connection_policy_statement = aws_iam.PolicyStatement(actions=[ 'events:RetrieveConnectionCredentials', ], effect=Effect.ALLOW, resources=[self.connection.connection_arn]) sm_policy_statement = aws_iam.PolicyStatement(actions= 'secretsmanager:DescribeSecret', 'secretsmanager:GetSecretValue' ], effect=Effect.ALLOW, resources=[self.connection.connection_secret_arn]) policy_document.add_statements(states_policy_statement) policy_document.add_statements(connection_policy_statement) policy_document.add_statements(sm_policy_statement) policy = aws_iam.Policy(self, 'APISFPolicy', document=policy_document) role = aws_iam.Role(self, 'StepFunctionServiceRole', description='API SF Service Role', assumed_by=aws_iam.ServicePrincipal('states.amazonaws.com')) role.attach_inline_policy(policy=policy) aws_iam.PolicyDocument( statements=[aws_iam.PolicyStatement(effect=aws_iam.Effect.ALLOW, actions=['sts:AssumeRole'], resources=[role.role_arn])])
oraz samą Step Function
def _build_step_function(self) -> sf.CfnStateMachine: return sf.CfnStateMachine( self, 'ApiEndpoints-StepFunction', definition_string=get_file(DEFINITION_FILE), role_arn=self.sf_role.role_arn, )
Po wykonaniu deploymedntu, powinniście dostać stack Cloud Formation podobny do mojego
a w sekcji Outputs dostępny będzie URL naszego endpointa ora Arn do APi Connection. Będziemy je przekazywali do Step Function.
Testujemy
Przy uruchamianiu naszej Step Function musimy podać jej URL do endpointa oraz Arn do roli. Zrobimy to za pomocą poniższego JSON-a, który przekazany zostanie jako input do stanu Call third-party API
{ "endpoint-url": "https://ts7hljufql.execute-api.eu-central-1.amazonaws.com", "connection-arn": "arn:aws:events:eu-central-1:455118752320:connection/ApiStepFunctionConnection25A5B4BC-861G4LRwlPss/d3a234b5-bc5a-41e3-a7a6-06f4d2b0baff", "body": "blah" }
Po wykonaniu funkcji, możemy sprawdzić działanie poszczególnych kroków. Ja skoncentruje się tutaj tylko na kroku Pass (tak wiem, niefortunna nazwa), który zdekoduje odpowiedź z serwera.
Na wejściu dostał on to, co zwrócił nasz serwer. W tym payload zapisany w formacie Base64:
{ "Headers": { "date": [ "Sun, 04 Feb 2024 07:25:35 GMT" ], "apigw-requestid": [ "SmZNejxgFiAEPPg=" ], "content-length": [ "24" ], "content-type": [ "application/json" ] }, "ResponseBody": { "data": "YmxhaA==" }, "StatusCode": 200, "StatusText": "OK" }
a zwrócił w parametrze data zdekodowaną informację z serwera
Podsumowanie
Nie każdy kierunek obrany przez AWS mi się podoba. Ale umożliwianie jak największej ilości rzeczy bez konieczności pisania kodu z pewnością jest jednym z tych, który do mnie trafia. I to bardzo. Im mniej kodu, tym mniej błędów. Poza tym, nie ma bezbłędnego kodu, jest tylko niedostatecznie przetestowany. A jestem przekonany, że kod działający dla nas, pod spodem, w AWS, jest lepiej przetestowany niż nasz, który musielibyśmy pisać.
Ten nowy ficzer 🙂 naprawdę ułatwi pracę ze Step Functions. Polecam wypróbowanie i być może wyrzucenie ze swoich rozwiązań własnoręcznych implementacji tego, co daje nam AWS.