Lambda@Edge
Artykuł ukazał się pierwotnie na blogu Chmurowiska.
Serverless to w minionym, 2018 roku słowo, o którym słyszał chyba każdy programista lub architekt systemów IT. Wszystko wskazuje na to, że rok 2019 będzie pod tym względem jeszcze ciekawszy. Wielu sądzi, że właśnie ten rok, to będzie czas, w którym rozwiązania serverless będą rozwijały się szybciej niż cokolwiek innego. No, może poza kolejnymi frameworkami Node.Js 😉
Jeżeli mowa o serverless, pierwsze co przychodzi do głowy to usługa AWS Lambda. Chyba każdy o niej słyszał. Nie każdy jednak wie, że Lambdy można uruchamiać także w usłudze CloudFront czyli w oferowanym przez Amazon rozwiązaniu content delivery network. CloudFront może działać nie tylko jako cache dla requestów sieciowych, może także uruchamiać funkcje Lambda. Możliwość ta to Lambda@Edge.
Jak uruchomić funkcje Lambda@Edge
Funkcje Lambda@Edge możemy wyzwalać za pomocą czterech eventów, które mamy do dyspozycji w CloudFront:
- Viewer Request
- Origin Request
- Origin Response
- Viewer Response

Viewer Request to event, który ma miejsce kiedy CloudFront otrzymuje request od użytkownika. Przed sprawdzeniem czy dane są w cache.
Origin Request wyzwalany jest tylko wtedy, gdy request przekazywany jest do źródła. Jeżeli potrzebne dane są już w cache, ten event nie jest wyzwalany.
Origin Response uruchamiany jest pomiędzy chwilą, w której CloudFront otrzymuje odpowiedź od źródła, a chwilą gdy odpowiedź umieszczana jest w cache. Co istotne, event wyzwalany jest także, gdy źródło zwróci błąd.
Viewer Response z kolei uruchamiany jest przed zwróceniem obiektu do użytkownika. Niezależnie od tego czy był w cache czy nie.
Wybór jest spory. Nasuwa się pytanie kiedy użyć poszczególnych eventów. W każdym przypadku trzeba do tego podejść indywidualnie. Jest jednak kilka wskazówek, które mogą pomóc w wyborze.
Jeżeli chcemy uruchomić naszą funkcję dla każdego requestu, skorzystajmy z eventu ViewerRequest lub ViewerResponse. Pierwszy będzie uruchomiony zawsze, drugi prawie za każdym razem.
W przypadku gdy chcemy zmienić sam request tak, że zmieni on odpowiedź źródła użyjmy eventu OriginRequest.
Chcąc zmienić obiekt, który zostanie zapisany w cache CloudFront użyjmy eventu OriginRequest lub OriginResponse.
Ograniczenia
Lambda@Edge ma w porównaniu z klasycznymi funkcjami Lambda wiele ograniczeń. Najważniejszym z nich, przynajmniej dla mnie to fakt, że takie funkcje możemy pisać tylko w Node.Js. Możemy zapomnieć np. o Pythonie. Przynajmniej dzisiaj. Mam nadzieję, że to się zmieni.
Zapomnijcie o layers I environment variables. Te drugie możemy zastąpić AWS Systems Manager Parameter Store. Ale zawsze wprowadzi to jakieś opóźnienia. A maksymalne timeouty w Lambda@Edge też mamy krótsze. Dla OriginRequest i OriginResponse to 30 sekund. Dla eventów ViewerReqeust oraz ViewerResponse to tylko 5 sekund. Mamy jeszcze więcej limitów. Na szczęście sporo z nich to tak zwane soft limits i możemy poprosić o ich zwiększenie. Także sama wielkość funkcji, rozmiar response oraz maksymalna ilość pamięci, którą możemy przydzielić funkcjom @Edge są mniejsze od standardowych.
Przykład
Spróbujemy zaimplementować przykładową funkcję Lambda@Edge. Nasze rozwiązanie pozwoli na generowanie za pomocą Lambdy strony www. Jej treść będziemy zmieniali w zależności od kraju, z którego łączy się nasz użytkownik.
Jeżeli wykorzystamy usługę CloudFront, czy to bezpośrednio czy też implementując nasze API w usłudze API Gateway jako Edge Optimized zostaną nam przekazane różne nagłówki w requeście. Między innymi nagłówek CloudFront-Viewer-Country.

Na podstawie właśnie tego nagłówka funkcja Lambda@Edge będzie generowała kod HTML zwracany do użytkownika.
Nasze rozwiązanie będzie się składało z kilku elementów:
- statycznej strony www w usłudze S3
- dystrybucji CloudFront
- funkcji Lambda podpiętej pod event OriginRequest
Proponuję zarówno koszyk S3 jak i funkcję Lambda utworzyć w regionie N.Virginia. Funkcje Lambda@Edge możemy tworzyć tylko w tym regionie.
Strona WWW
W pierwszym kroku musimy utworzyć bucket S3, wgrać do niego poniższy kod i uruchomić na tym koszyku Web Site Hosting.
Kod strony www, która będzie serwowana z S3 jest bardzo prosty.
<!DOCTYPE html />
<html lang="en">
<head>
<meta charset="utf-8">
<title>Chmurowisko: Hello!</title>
</head>
<body>
<h1>Hello <strong>unknown</strong> from S3</h1>
</body>
</html>
Strona po prostu będzie informowała, że nie można rozpoznać kraju, z którego użytkownik łączy się z naszą usługą. Do tej strony „podepniemy” też dystrybucję CloudFront.

Cała magia będzie się rozgrywała w funkcji Lambda@Edge umieszczonej w CloudFront. To ona będzie generowała odpowiedź, która będzie różna w zależności od kraju, z którego nasz użytkownik pochodzi.
Lambda
W kolejnym kroku musimy utworzyć funkcję Lambda. Pamiętajcie o regionie, N.Virginia.
Tworzymy zwykłą Lambdę, jako runtime wybieramy Node.Js 8.10. Wystarczy domyślna rola, która nada uprawnienia do zapisu logów w usłudze CloudWatch.
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": [
"logs:CreateLogGroup",
"logs:CreateLogStream",
"logs:PutLogEvents"
],
"Resource": "arn:aws:logs:*:*:*"
}
]
}
Musimy dodać tylko do Trust relationships Lambda@Edge.

Po zmianach polityka powinno wyglądać to tak:
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Principal": {
"Service": ["lambda.amazonaws.com", "edgelambda.amazonaws.com"]
},
"Action": "sts:AssumeRole"
}
]
}
Kod samej funkcji jest prosty. W nagłówkach wyszukuje kraj, z którego łączy się nasz użytkownik i wstawia go do kodu HTML, który jest zwracany do użytkownika. Jeżeli nagłówka nie ma, zwraca po prostu request.
'use strict';
exports.handler = (event, context, callback) => {
const request = event.Records[0].cf.request;
const headers = request.headers;
console.log('request', request);
if (
headers['cloudfront-viewer-country'] &&
headers['cloudfront-viewer-country'][0].value
) {
const cloudFrontCountryCode = headers['cloudfront-viewer-country'][0].value;
console.log('countryCode', cloudFrontCountryCode);
const response = {
status: '200',
statusDescription: 'OK',
headers: {
'cloudfront-viewer-country': [
{
key: 'CloudFront-Viewer-Country',
value: cloudFrontCountryCode,
},
],
'cache-control': [
{
key: 'Cache-Control',
value: 'max-age=100',
},
],
'content-type': [
{
key: 'Content-Type',
value: 'text/html',
},
]
},
body: `
<!DOCTYPE html />
<html lang="en">
<head>
<meta charset="utf-8">
<title>Chmurowisko Lambda@Edge</title>
</head>
<body>
<h1>Hello <strong>${cloudFrontCountryCode}</strong> from Lambda @Edge!</h1>
</body>
</html>
`,
};
callback(null, response);
}
callback(null, request);
};
Aby nasza Lambda mogła być wykorzystana jako funkcja Lambda@Edge musimy opublikować jej wersję. W usłudze CloudFront musimy podpiąć ARN opublikowanej wersji.
W konsoli wybieramy menu Actions i opcję Publish new version. Możemy dodać jakiś opis i po chwili nasza wersja będzie gotowa.

O wersjach i aliasach funkcji Lambda pisałem już na naszym blogu.
Po utworzeniu wersji, na górze konsoli wyświetli nam się ARN do naszej funkcji.

Skopiujmy go sobie, będzie potrzebny za chwilę.
Dystrybucja CloudFront
Mamy nasz bucket, mamy Lambdę, stwórzmy teraz powiązaną z nimi dystrybucję CloudFront.
- Jako origin wybieramy nasz bucket. To z niego będą pobierane w razie potrzeby dane.
- Przekierowujemy też wywołania HTTP na HTTPS.

- W kolejnym kroku musimy dodać do whitelisty nagłówek, którym się będziemy posługiwali.

- Teraz „najważniejsze” Musimy podpiąć funkcję Lambda pod dystrybucję. Wybieramy interesujący nas event, czyli OriginRequest i wklejamy ARN do naszej funkcji Lambda. Pamiętajcie, że powinien to być ARN do opublikowanej wersji.

- I to właściwie wszystko. Klikamy Create i nasza dystrybucja zacyna się tworzyć.

Po 15-30 minutach powinna powinna być gotowa. Możemy przejść do testowania naszego rozwiązania.
Testy i logi
Każda dystrybucja CloudFront ma swoje DomainName. Skorzystajmy z przeglądarki Chrome i przetestujmy działanie.
Włączamy narzędzia programisty w Chrome, wklejamy adres naszej dystrybucji w przeglądarce i… Voilà!!!

Dostaliśmy odpowiedź od naszej funkcji Lambda@Edge.
W przeglądarce, w zakładce Network klikamy w response z serwera i sprawdźmy nagłówki.

Sprawdźmy do mamy w logach. Wchodzimy do usługi CloudWatch w regionie N.Virginia i… Nic tam nie ma. Na początku także mnie to zdziwiło. Lambda@Edge zapisuje logi w regionie, w którym jest wywoływana. Albo w najbliższym. Ja łączyłem się z Polski, sprawdźmy więc we Frankfurcie.

Jak widać wszystko jest w porządku. Nasza Lambda zadziałała. Logi zostały zapisane.
Odświeżmy stronę i sprawdźmy co się stanie

Tym razem nasza strona została załadowana z cache, bez uruchamiania funkcji Lambda@Edge. W logach nie będziemy mieli nic nowego.
Spróbujmy się połączyć z innego kraju. Najlepiej wykorzystać do tego jakieś połączenie VPN. Ja użyję PureVPN ale możecie użyć np. TunnelBear. Na nasze potrzeby wystarczy i jest darmowy.
Ja połączyłem się z serwerem VPN w Holandii.

Kolejna próba w Chrome i jesteśmy w Holandii.

Tym razem nagłówek x-cache
miał wartość Miss from cloudfront
. Dlaczego? Było to pierwsze wywołanie z Holandii i CloudFront nie miał jeszcze w cache odpowiedniej odpowiedzi. Ponownie zadziałała nasza Lambda@Edge.

Usuwamy zasoby
Usuwanie zasobów związanych z Lamda@Edge jest trochę upierdliwe. Pamiętajcie, że nasze funkcje replikowane są do poszczególnych miejsc docelowych. W moim przypadku, przy kasowaniu Lambda@Edge zadziałał taki scenariusz:
- Odpinamy funkcję od dystrybucji CloudFront. Można to zrobić w ustawieniach dystrybucji w sekcji Behaviour.
- Czekamy przynajmniej 30 minut. Raz musiałem poczekać prawie 2 godziny.
- Usuwamy funkcję.
Jeżeli używamy CloudFormation to prawie na pewno będzie konieczna dwukrotna próba usunięcia stacka. Za pierwszym razem operacja usunięcia funkcji się nie powiedzie.
Podsumowanie
Udostępniona w 2017 roku usługa Lambda@Edge jest wbrew pozorom potężnym narzędziem, dla którego można znaleźć wiele zastosowań.
Za jej pomocą możemy na przykład sprawdzać wartości cookies w requestach i na tej podstawie przekierowywać użytkowników na inne adresy URL. Na podstawie nagłówka User-Agent możemy serwować różne rozmiary zdjęć. Nie będzie wielkim problemem także wykonanie autoryzacji requestów.
W dokumentacji Lambda@Edge można znaleźć wiele przykładów na zastosowanie tej usługi.