Ten artykuł to kontynuacja wpisu Jak zarządzać sekretami w AWS?, więc jeśli jeszcze go nie czytałeś to zacznij właśnie od niego. Tym razem przedstawię Ci praktyczne metody pobierania sekretów do naszych funkcji Lambda z usług SSM Parameter Store oraz Secrets Manager. Skupimy się na użyciu AWS-SDK oraz bardzo popularnej w świecie serverless biblioteki Middy.
Wszystkie przykłady z tego artykuły umieściłem na githubie w projekcie sample-secrets-handling.
Bezpieczeństwo i dostępy
Zanim przejdziemy do praktycznych przykładów bardzo ważne jest, abyś ustawił odpowiednie dostępy dla roli, która będzie użyta przez Twoje funkcje Lambda. Ze względu na to czy będziesz używać SSM Parameter Store czy Secrets Managera to są to oczywiście inne zasoby i akcje.
W artykule (i przykładowej aplikacji) posługuję się dwoma sekretami, odpowiednio po jednym na każdą z usług. Dostępy do niech są zdefiniowane w sekcji provider/iamRoleStatements
w pliku serverless.yml
.
Jeśli chcesz ruchomić projekt na swoim koncie to musisz ręcznie dodać we Frankfurcie dwa sekrety:
/sample/mysecret
w SSM Parameter Storesample/my-second-secret
w Secrets Manager z kluczemsecret
.
1 | provider: |
Już na samym wstępie, przy konfiguracji roli mamy kilka ciekawostek, które mogą doprowadzić do irytacji.
Po pierwsze Middy - co to jest wyjaśnię później - korzysta wewnętrznie z innych metod API AWS, dlatego potrzebuje innych przywilejów do pobrania parametru z SSM niż równoważna metoda korzystająca wprost z AWS-SDK (linijka 6
vs 7
).
Po drugie, sekret zdefiniowany w Secrets Managerze ma ARN składające się z nazwy sekretu oraz randomowego suffixa. Dlatego w ostatniej linijce na samym końcu jest *
, po to, aby nie trzeba było znać tego suffixa. Niekoniecznie to jest oczywiste, szczególnie, że nie jest spójne z SSM Parameter Store. Wykorzystanie *
może bardzo ułatwić życie, szczególnie wtedy gdy tworzymy sekrety automatycznie przez API. Traktują tę gwiazdkę jak taki life-hack 😁
Jak pobrać sekrety do funkcji Lambda?
Przygotowałem dla Ciebie, aż pięć przykładów, każdy w osobnej funkcji lambda.
Pobranie sekretu z usługi:
- SSM Parameter Store przy użyciu AWS-SDK
- SSM Parameter Store przy użyciu AWS-SDK i skeszowanie go pomiędzy wywołaniami funkcji
- SSM Parameter Store przy użyciu Middy i skeszowanie go pomiędzy wywołaniami funkcji
- Secrets Manager przy użyciu AWS-SDK i skeszowanie go pomiędzy wywołaniami funkcji
- Secrets Manager przy użyciu Middy i skeszowanie go pomiędzy wywołaniami funkcji
1. Pobranie sekretu z usługi SSM Parameter Store przy użyciu AWS-SDK
Kod przykładu jest bardzo prosty. W handler
ze wywołujemy funkcję getSsmSecret
z nazwą parametru. Tak, cała ta “ścieżka” to pojedyncza nazwa.
1 | const SSM = require('aws-sdk/clients/ssm') |
Korzystamy z metody API AWS.SSM.getParameter()
do której, co ważne, musimy przekazać obiekt składający z dwóch pól, nazwy Name
oraz WithDecryption
ustawionego na true
, gdyż nasz parametr w SSM jest oczywiście zaszyfrowany. W przeciwnym wypadku, dostalibyśmy zaszyfrowanego stringa. I w sumie to wszystko 🙂
To jest najprostszy przykład, stąd też funkcja Lambda nazywa się ssmSimple
.
Wywołując tę funkcje dwukrotnie pod rząd (sls invoke -f ssmSimple -l
), otrzymasz tego typu wyniki jak poniżej. Zwróć uwagę, że w logach za każdym razem mamy Getting secret from SSM
.
1 | $ sls invoke -f ssmSimple -l |
Cache’owanie
Pomimo iż rozwiązanie działa prawidłowo to jego minusem jest to, że przy każdym wywołaniu funkcji, Lambda strzela do API usługi SSM Parameter Store. Co przy dużym wolumenie może skutkować throttling’iem (błąd 429
), czego oczywiście nie chcemy, dlatego warto umieścić pobraną wartość w cache’u tak, aby kolejna inwokacja rozgrzanej funkcji Lambda już tego nie robiła. Dzięki czemu, skrócimy też czas działania kolejnych wywołań.
Skąd wziąć cache? Najprostszy będzie w pamięci funkcji Lambda.
2. Pobranie sekretu z Parameter Store przy użyciu AWS-SDK + cache
Kod drugiego przykładu bazuje na poprzednim. Jedyne co się zmienia to to, że wartość sekretu przypisujemy do zmiennej w przestrzeni globalnej (linijka 3
i 15
), czyli poza handler
‘em funkcji Lambda.
1 | const SSM = require('aws-sdk/clients/ssm') |
Tym razem jak uruchomimy naszą funkcję dwukrotnie to tylko raz zobaczymy Getting secret from SSM
.
1 | $ sls invoke -f ssmSimpleCache -l |
Jeśli nie rozumiesz, jak ta wartość się skeszowała to przeczytaj Co to jest cold start Lambdy? Wyjaśnię Ci go w 4 minuty, szczególnie paragraf Cold start a skalowalność.
Zanim przejdziemy dalej to musimy odpowiedzieć na jedno pytanie.
Co to jest Middy?
Middy to bardzo popularna w serverless biblioteka, dostępna pod adresem https://middy.js.org.
Jest to tak zwany middleware, gdyż działa pomiędzy wywołaniem handler
a funkcji Lambda, a naszym kodem biznesowym wewnątrz tej funkcji. Czyli jest gdzieś pośrodku, stąd też nazwa 🤔
Troszkę przypomina to aspekty np. w Java, pozwala się wpiąć z kodem przed uruchomieniem innej metody, w tym wypadku handler
a.
Możemy sobie przypiąć kilka takich middleware do naszej funkcji i one się wywołają przed lub po naszym handler
ze. W przykładach poniżej korzystam tylko z jednego middleware na raz, wywoływanego przed uruchomieniem handler
a. Są to odpowiednio moduły do obsługi ssm oraz secrets-manager.
3. Pobranie sekretu z SSM Parameter Store przy użyciu Middy + cache
Korzystając z Middy, kod naszej funkcji Lambda wygląda zdecydowanie inaczej.
1 | const middy = require('@middy/core') |
Funkcja handler
(linijka 4
) to tak na prawdę wynik owrapowania lub dekoracji biblioteką Middy anonimowej funkcji, która przyjmuje dwa parametry event
oraz context
. Są one potrzebne, gdyż w context
został ustawiony nasz sekret.
Skąd on się tam wziął?
Sekret został ustawiony, przy wykorzystaniu modułu ssm
(linijka 9
). Odpowiada za to właśnie Middy. Zanim zostanie wywołany “nasz kod” (linijki 5
i 6
) w funkcji handler
Middy łączy się z SSM Parameter Store i pobiera dla nas sekret w momencie startu funkcji Lambda.
Proste? Niekoniecznie.
Wygodne? Pewnie!
4. Pobranie sekretu z Secrets Manager przy użyciu AWS-SDK + cache
Ostatnie dwa przykłady dotyczą pobierania sekretów zdefiniowanych w usłudze Secrets Manager. Koncepcja jest taka sama jak przy SSM Parameter Store, natomiast różni się w szczegółach.
Poniższy kod nawiązuje do drugiego przykładu. Zasada działania i keszowania wartości jest tożsama.
1 |
|
Natomiast to gdzie mamy największą różnicę to fakt, że sekret jest zwrócony do nas przez AWS jako JSON i musimy go sparsować (linijka 16
).
Również na komentarz zasługuje słowo secret
w tej samej linijce. Jest to nazwa pola (klucz) w JSONie, który jest zapisany w moim sekrecie w Secrets Manager. Może to być dowolna nazwa wybrana przez Ciebie. Co więcej, możemy mieć wiele takich par klucz wartości
w jednym sekrecie. Np. wszystkie dane potrzeba do połączenia się z bazą danych lub jakimś zewnętrznym API.
5. Pobranie sekretu z Secrets Manager przy użyciu Middy
I przyszła pora na ostatni przykład.
1 | const middy = require('@middy/core') |
Jest bardzo podobny do trzeciego.
Lecz tym razem korzystamy z innego modułu rozszerzającego Middy (linijka 2
). Reszta jest analogiczna, zwracam tylko uwagę na linijkę 11
. Dla SSM Parameter Store mamy names
, a przy Secrets Manager używamy secrets
do podania listy sekretów, które ma nam Middy pobrać.
Taki szczegół można łatwo przeoczyć przy migracji na inne rozwiązanie do przechowywania sekretów i stracić sporo czasu na debugowanie. Dlatego uczulam na tę drobną różnicę.
Posumowanie
Mam nadzieję, że powyższe przykłady jasno tłumaczą w jaki sposób pobierać sekrety o których rozpisałem się w poprzednim artykule.
Wszystkie użyte przykłady umieściłem na githubie w kompletnym projekcie sample-secrets-handling. Możesz dowolnie kopiować ten kod, tak aby błyskawicznie skorzystać z sekretów w Twoim projekcie.
Powodzenia w tworzeniu bezpiecznych aplikacji 😃