Jak podłączyć dysk EFS do funkcji Lambda?


"Jak podłączyć dysk EFS do funkcji Lambda?"

Elastic File System jest już dostępny dla Lambdy. Co prawda sama premiera miała miejsce ponad miesiąc temu, ale ja dopiero teraz znalazłem czas, aby opisać to rozwiązanie na łamach Serverless Polska. Jakiś miesiąc temu wykorzystałem EFS wraz ze Step Functions do budowy procesu ETL zasilającego nasze jezioro danych w projekcie u klienta. Było trochę z tym zabawy i dlatego uważam, że warto to opisać, aby inni nie musieli się męczyć. Mam nadzieje, że ta wiedza pozwoli Ci odkryć nowe możliwości serverless. 😀

Co to jest AWS EFS?

Na wstępie wyjaśnijmy z czym mamy do czynienia. AWS EFS to coś na kształt dysku sieciowego, który można podłączyć do wielu urządzeń równocześnie. EFS to stara usługa, dotychczas wspierała instancje EC2 oraz kontenery. Dopiero od niedawna integruje się z AWS Lambda.

Po skonfigurowaniu wszystkiego w infrastrukturze (oczywiście jako kod), dysk EFS staje się dostępny dla funkcji Lambda najzwyczajniej w świecie na ścieżce /mnt/twoja_nazwa_dysku.

Co to zmienia?

Jakie nowe scenariusze dzięki tej integracji wchodzą w grę?

Po pierwsze i najważniejsze, dysk EFS, praktycznie o nieograniczonej pojemności, likwiduje ograniczenie związane z brakiem miejsca dla funkcji Lambda. Normalnie ma ona do dyspozycji 500MB w katalogu /tmp. Z dyskiem EFS możemy swobodnie przekroczyć tę granicę. Dzięki czemu możemy pracować z dużymi plikami!

Wszystkim fanom uczenia maszynowego na pewno zaświeciły się teraz oczy!

Zgadza się, Machine Learning to jeden z głównych use caseów odblokowanych nową funkcjonalnością. W sieci już pojawiają się doniesienia na temat tego, co udało się osiągnąć dzięki połączeniu EFS i Lambdy. Dalej, oczywistym jest przetwarzanie wszelkich dużych zasobów, chociażby konwersja plików wideo.

Co więcej, katalog /tmp jest integralną częścią każdego kontenera, w którym uruchamia się dana instancja funkcji Lambda. Natomiast, EFS jest współdzielony między wieloma urządzaniami bądź funkcjami Lambda. To oznacza, że raz zapisany plik jest dostępny dla wszystkich użytkowników dysku oraz nie zniknie po tym jak rozgrzana funkcja zostanie wyłączona przez usługę AWS Lambda.

Nieograniczoną przestrzeń możemy też wykorzystać do trzymania wszelakich bibliotek, które potem załaduje sobie i użyje kod naszej Lambdy. Choćby we wspomnianym wcześniej machine learningu, gdzie biblioteki są słusznych rozmiarów.

Po drugie integracja z usługą EFS daje nam alternatywę wobec używania AWS S3. W pewnych scenariuszach dostęp do zwykłego file systemu okazuje się być szybszy, wygodniejszy oraz tańszy. EFS jest rozliczany tylko za ilość miejsca, z którego korzystamy (chyba, że wybierzemy ekstra ficzery), a S3 poza tym liczy sobie opłaty za każdy zapis i odczyt (PUT i GET). W wielu przypadkach częsty zapis i odczyt do S3 generuje większe koszty niż za zużyte miejsce.

Między innymi dlatego wykorzystałem EFS w swojej ostatniej aplikacji, która pobiera dane z różnych źródeł i umieszcza je w moim data lake. W połączeniu z Lambdą i Step Functions świetnie się do tego nadaje.

Wady EFS

Oczywiście jak to w architekturze bywa, są tutaj poważne trade-offy. EFS to tylko zwykły filesystem. Nie ma tam żadnych eventów, zatem zapomnij o wywołaniu automatycznie funkcji Lambda po tym jak ktoś wgra plik na dysk. Provisioning i konfiguracja dysku jest mocno toporna, a w porównaniu do S3 to istna katorga o czym zaraz się przekonasz sam 😉

Jak podłączyć dysk EFS do funkcji Lambda?

Na chwilę obecną nie ma jeszcze oficjalnego wsparcia w Serverless Framework (choć w CloudFormation i SAM już jest) - jestem pewien, że gdy je wprowadzą to będzie dużo wygodniej dodać EFS do funkcji. Jednak póki, co dzielę się swoim rozwiązaniem, które opracowałem na bazie artykułów Yana Cui, Jamesa Beswicka oraz Petera Sbarskiego.

Po pierwsze VPC

Funkcja Lambda, aby móc się połączyć z dyskiem EFS musi być w tym samym VPC, co i dysk. Od 2019 roku to nie problem, gdyż AWS mocno zniwelował cold starty w VPC.

Aby funkcja uruchamiała się w VPC wystarczy podać idki subnetów i Security Group w konfiguracji w pliku serverless.yml

1
2
3
4
5
6
7
8
9
functions:
writeToEfs:
handler: src/writeToEfs/function.handler
vpc:
securityGroupIds:
- sg-xxxxxxxx
subnetIds:
- subnet-xxxxxxxx
- subnet-xxxxxxxx

Po drugie przywileje

Rola z którą będzie uruchamiana funkcja Lambda musi mieć nadane odpowiednie prawa. Kopiując od Yana używam następujących:

1
2
3
4
5
6
7
8
9
10
11
12
13
provider:
# other configuration
iamRoleStatements:
- Effect: Allow
Action:
- ec2:CreateNetworkInterface
- ec2:DescribeNetworkInterfaces
- ec2:DeleteNetworkInterface
- elasticfilesystem:ClientMount
- elasticfilesystem:ClientRootAccess
- elasticfilesystem:ClientWrite
- elasticfilesystem:DescribeMountTargets
Resource: '*'

Sam dysk EFS

Stworzenie dysku nie jest w cale takie proste jak mogłoby się wydawać i daleko mu do wygody tworzenia i używania bucketów S3. W sekcji resources pliku serverless.yml zdefiniuj następujące zasoby:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
resources:
Resources:
NetworkDrive:
Type: AWS::EFS::FileSystem
Properties:
FileSystemTags:
- Key: Name
Value: LambdaDrive-${self:provider.stage}

MountTargetResourceA:
Type: AWS::EFS::MountTarget
Properties:
FileSystemId: !Ref NetworkDrive
SubnetId: subnet-xxxxxxxx # change that value to your id
SecurityGroups:
- !GetAtt MountPointSecurityGroup.GroupId

MountTargetResourceB:
Type: AWS::EFS::MountTarget
Properties:
FileSystemId: !Ref NetworkDrive
SubnetId: subnet-xxxxxxxx # change that value to your id
SecurityGroups:
- !GetAtt MountPointSecurityGroup.GroupId

MountPointSecurityGroup:
Type: AWS::EC2::SecurityGroup
Properties:
GroupDescription: Security group to allow NFS - Lambda communication.
VpcId: vpc-xxxxxxx
SecurityGroupIngress:
- IpProtocol: tcp
FromPort: 2049
ToPort: 2049
SourceSecurityGroupId: sg-xxxxxxxx # change that. Same as one for Lambda
SecurityGroupEgress:
- IpProtocol: '-1'
CidrIp: 0.0.0.0/0

AccessPointResource:
Type: AWS::EFS::AccessPoint
Properties:
FileSystemId: !Ref NetworkDrive
PosixUser:
Uid: 1001
Gid: 1001
RootDirectory:
CreationInfo:
OwnerGid: 1001
OwnerUid: 1001
Permissions: 770
Path: /efs

Dlaczego tego aż tyle? Sam się zastanawiam. 🤔
Najpierw mamy sam dysk EFS o logicznej nazwie NetworkDrive, niestety bez pozostałych rzeczy jest on kompletnie bezużyteczny. Musimy go podpiąć do jakiejś wirtualnej sieci, stąd następnie mamy MountTargetResourceA i MountTargetResourceB, które to pozwalają dostać sie do niego z podanych subnetów. Następnie mamy MountPointSecurityGroup, która jest niezbędna bo inaczej ruch sieciowy będzie zablokowany dla portu, którego używa EFS.

Jakoś nie wiedzieć czemu, nikt z wyżej wymienionych autorów nie wspominał o tym. Być może dlatego, że większość z nich wyklikała swoje dyski, a nie budowała je zgodnie z podejściem Infrastruktura jako kod. Ja na tym straciłem parę godzin 😤

Bardzo ważne jest, aby podać prawidłowy (duh!) id Security Grupy w parametrze SourceSecurityGroupId - to jest ta sama grupa, którą przypisaliśmy do funkcji lambda na samym początku.

Ostatnim elementem jest AccessPointResource. I to akurat AWS sprytnie wymyślił. Ponieważ do dysku sieciowego z założenia będzie się łączyć wielu klientów to potrzebujemy jakiś sposób zarządzania użytkownikami i dostępami do plików w tym dysku. I taki AccessPointResource jest abstrakcją, którą właśnie do tego wykorzystamy. Takich punktów dostępowych może być wiele, ja natomiast wykorzystałem jeden dla wszystkich moich funkcji Lambda. Wbrew pozorom tutaj wiele się dzieje i ta konfiguracja jest ściśle powiązana z NetworkDrive zdefiniowanym na początku. Jak powiedziałem tutaj określamy prawa dostępu i w zależności od nich AWS będzie lub nie będzie w stanie zainicjalizować dla nas file system przy pierwszym użyciu. Stąd Permissions ustawione na 770 (kłania się chmod). Dzięki temu zostanie stworzony dla nas katalog (folder) /efs przy innych prawach może się on nie stworzyć! Więcej informacji w dokumentacji.

Łączenie dysku EFS z funkcją Lambda

Na chwilę obecną nie da się tego zrobić elegancko w Serverless Framework. Czekając na oficjalne wsparcie możemy się posłużyć mało znaną funkcjonalnością o nazwie extensions, która pozwala na modyfikowanie ustawień funkcji Lambda stworzonych w sekcji functions.

1
2
3
4
5
6
7
8
9
10
11
12
13
resources:
Resources:
NetworkDrive:
# ...
# ...
# ...

extensions:
WriteToEfsLambdaFunction:
Properties:
FileSystemConfigs:
- Arn: !GetAtt AccessPointResource.Arn
LocalMountPath: /mnt/efs

Tutaj już mocno zaglądamy pod maskę Serverless Framework.

Gratulacje, stałeś się właśnie specjalistą!

Ale właściwie co tu się dzieje? To jest kawałek CloudFormation, który jest “doklejany” do wygenerowanego przez Serverless Framework kodu dla funkcji Lambda, którą zdefiniowaliśmy na samym początku i nazwaliśmy writeToEfs. Kluczowym jest nazwa naszego rozszerzenia, bez wchodzenia w szczegóły tego jak działa SF konwencja jest taka “nazwa-funkcji-z-wielkiej-litery”+”LambdaFunction”. W ten sposób writeToEfs staje się WriteToEfsLambdaFunction. Dalej w tej sekcji podajemy referencje do AccessPointResource i mówimy, że dysk ma być zamontowany pod ścieżką /mnt/efs. To jest akurat standardowe linuxowe montowanie dysku, w końcu Lambdy działają w Amazon Linux.

Odpalamy sls deploy

To już cała konfiguracja potrzebna do podłączenia dysku EFS do funkcji Lambda (zakładając, że masz już VPC). Niestety w moim projekcie deployment takiej konfiguracji nie działał. Musiałem go robić w dwóch krokach.

  1. W pierwszym zakomentowałem sekcje extensions i zrobiłem sls deploy, które się powiodło.
  2. W drugim odkomentowałem ten fragment i znów uruchomiłem deployment. W ten sposób funkcje Lambda zostały prawidłowo zaktualizowane i miały dostęp do dysku EFS.

Podsumowanie

Mam nadzieję, że ten tutorial ułatwi Ci skonfigurowanie dysku EFS w Twoim projekcie. Jak widzisz, nie jest to proste ale raz skonfigurowane działa bezproblemowo.

W swoim projekcie jestem zadowolony z takiego rozwiązania. Mam skonfigurowane 4 różne funkcje Lambda dokładnie w taki sposób. Jedyna różnica jest taka, że każda z nich ma swoją konfigurącję w sekcji extensions, jednak wszystkie korzystają z tego samego AccessPointResource.

Powodzenia 😀