Serverless FrameworkでEFS for AWS Lambdaをデプロイする

Lambdaで発生するこちらのエラー。 /tmp 領域を使った処理などでディスク不足で発生するのですが、特殊なファイル形式など、どうしてもファイルを扱いたい場面には悩まされることありました。

OSError: [Errno 28] No space left on device

少し前の発表ですが、「Amazon Elastic File System for AWS Lambda」というのが発表さたので、LambdaからEFSをマウントすることでLambdaのディスク枯渇問題は回避可能になりました。

EFSの作成含めServerless Frameworkからデプロイしてみるサンプルを記載します。

aws.amazon.com

コード

Lambda関数 (handler.py)

EFSのマウント情報を出力する処理を定義します。

import os
import subprocess
from typing import List

EFS_MOUNT_DIR = os.getenv("EFS_MOUNT_DIR")


def exec_command(cmd: List[str]):
    print(f"Command -> {cmd} -------------------------------------------")
    out = subprocess.run(cmd, stdout=subprocess.PIPE)
    print(out.stdout.decode())


def lambda_handler(event, context):
    exec_command(["df", "-h"])
    exec_command(["cat", "/proc/mounts"])
    exec_command(["touch", f"{EFS_MOUNT_DIR}/{os.uname().nodename}"])
    exec_command(["ls", "-l", EFS_MOUNT_DIR])

設定ファイル(.env)

useDotenv: true を設定してるので .env ファイルをServerless Frameworkの設定ファイルとして定義します。

AWS_REGION=ap-northeast-1
VPC_ID=vpc-xxxxxxxx
LAMBDA_SUBNET_ID=subnet-xxxxxxxx
EFS_MOUNT_DIR=/mnt/efsdata
EFS_ROOT_DIR=/efsdata

Serverless Framework定義(serverless.yml)

Serverless Frameworkの定義ファイルです。

service: lambda-efs-mount-sample
frameworkVersion: "2"
useDotenv: true

provider:
  name: aws
  runtime: python3.8
  region: ${env:AWS_REGION}
  stage: prod
  lambdaHashingVersion: 20201221

functions:
  func:
    handler: handler.lambda_handler
    environment:
      EFS_MOUNT_DIR: ${env:EFS_MOUNT_DIR}
    fileSystemConfig:
      localMountPath: ${env:EFS_MOUNT_DIR}
      arn: !GetAtt ["EFSAccessPoint", "Arn"]
    vpc:
      subnetIds:
        - ${env:LAMBDA_SUBNET_ID}
      securityGroupIds:
        - !GetAtt ["LambdaSecurityGroup", "GroupId"]
    dependsOn: EFSMountTarget

resources:
  Resources:
    LambdaSecurityGroup:
      Type: AWS::EC2::SecurityGroup
      Properties:
        GroupDescription: Lambda Access for EFS
        VpcId: ${env:VPC_ID}
        Tags:
          - Key: Name
            Value: LambdaEFSSecurityGroup

    EFSSecurityGroup:
      Type: AWS::EC2::SecurityGroup
      Properties:
        GroupDescription: EFS Allowed Ports
        VpcId: ${env:VPC_ID}
        SecurityGroupIngress:
          - IpProtocol: tcp
            FromPort: 2049
            ToPort: 2049
            SourceSecurityGroupId: !GetAtt ["LambdaSecurityGroup", "GroupId"]
            Description: from Lambda
        Tags:
          - Key: Name
            Value: EFSSecurityGroup

    EFSFileSystem:
      Type: AWS::EFS::FileSystem
      Properties:
        FileSystemTags:
          - Key: Name
            Value: MyFileSystem
        BackupPolicy:
          Status: ENABLED
        Encrypted: true
        LifecyclePolicies:
          - TransitionToIA: AFTER_30_DAYS
        PerformanceMode: generalPurpose

    EFSMountTarget:
      Type: AWS::EFS::MountTarget
      Properties:
        FileSystemId: !Ref EFSFileSystem
        SecurityGroups:
          - !Ref EFSSecurityGroup
        SubnetId: ${env:LAMBDA_SUBNET_ID}
      DependsOn: EFSFileSystem

    EFSAccessPoint:
      Type: AWS::EFS::AccessPoint
      Properties:
        FileSystemId: !Ref EFSFileSystem
        PosixUser:
          Uid: 1001
          Gid: 1001
        RootDirectory:
          Path: ${env:EFS_ROOT_DIR}
          CreationInfo:
            OwnerGid: 1001
            OwnerUid: 1001
            Permissions: 750
        AccessPointTags:
          - Key: Name
            Value: MyAccessPoint
      DependsOn: EFSFileSystem

ポイント

ポイントを少し解説書きます。

LambdaのEFSマウント設定

Lambdaの定義のところで fileSystemConfig というセクションを作ることでLambdaローカルのマウントディレクトリと、EFSのアクセスポイントを定義可能です。

EFSのアクセスポイントは resources セクションのCFnの定義から引用できるのが便利です。

functions:
  func:
#省略
    fileSystemConfig:
      localMountPath: ${env:EFS_MOUNT_DIR}
      arn: !GetAtt ["EFSAccessPoint", "Arn"]
#省略
LambdaのDependsOn設定

EFSのマウントターゲットは作成まで時間がかかります。先にServerless FrameworkからLambdaの生成が走るとエラーとなるので dependsOn を設定しておきます。

functions:
  func:
#省略
    dependsOn: EFSMountTarget
LambdaのUID/GID

Lambdaの実行ユーザは、UID=1001 GID=1001 なのでアクセスポイントにも、それを受けた設定を行います。

    EFSAccessPoint:
      Type: AWS::EFS::AccessPoint
      Properties:
        FileSystemId: !Ref EFSFileSystem
        PosixUser:
          Uid: 1001
          Gid: 1001
        RootDirectory:
          Path: ${env:EFS_ROOT_DIR}
          CreationInfo:
            OwnerGid: 1001
            OwnerUid: 1001
            Permissions: 750

実行してみる

Lambdaを実行して、ログを確認してみます。

実行

$ sls invoke -f func                                                                                                                                             
null
$ sls logs -f func

結果確認

/mnt/efsdata ディレクトリにディスクが割り当たっているのが確認できます。

Command -> ['df', '-h'] -------------------------------------------
Filesystem                                                      Size  Used Avail Use% Mounted on
/mnt/root-rw/opt/amazon/asc/worker/tasks/rtfs/python3.8-amzn-2  9.8G  8.4G  1.4G  87% /
/dev/vdb                                                        1.5G   14M  1.4G   1% /dev
/dev/vdd                                                        526M  872K  514M   1% /tmp
/dev/root                                                       9.8G  8.4G  1.4G  87% /etc/passwd
/dev/vdc                                                        128K  128K     0 100% /var/task
127.0.0.1:/                                                     8.0E     0  8.0E   0% /mnt/efsdata

nfs の設定が入っています。

Command -> ['cat', '/proc/mounts'] -------------------------------------------
/mnt/root-rw/opt/amazon/asc/worker/tasks/rtfs/python3.8-amzn-2 / overlay ro,nosuid,nodev,relatime,lowerdir=/tmp/es073328941/6c4d99488764391:/tmp/es073328941/b9f5c9402371752 0 0
/dev/vdb /dev ext4 rw,nosuid,noexec,noatime,data=writeback 0 0
/dev/vdd /tmp ext4 rw,relatime,data=writeback 0 0
none /proc proc rw,nosuid,nodev,noexec,noatime 0 0
/dev/vdb /proc/sys/kernel/random/boot_id ext4 ro,nosuid,nodev,noatime,data=writeback 0 0
/dev/root /etc/passwd ext4 ro,nosuid,nodev,relatime,data=ordered 0 0
/dev/root /var/rapid ext4 ro,nosuid,nodev,relatime,data=ordered 0 0
/dev/vdb /etc/resolv.conf ext4 ro,nosuid,nodev,noatime,data=writeback 0 0
/dev/vdb /mnt ext4 rw,noatime,data=writeback 0 0
/dev/vdc /var/task squashfs ro,nosuid,nodev,relatime 0 0
127.0.0.1:/ /mnt/efsdata nfs4 rw,relatime,vers=4.1,rsize=1048576,wsize=1048576,namlen=255,hard,noresvport,proto=tcp,port=20372,timeo=600,retrans=2,sec=sys,clientaddr=127.0.0.1,local_lock=none,addr=127.0.0.1 0 0

ファイルの書き込みもできているようです。

Command -> ['touch', '/mnt/efsdata/169.254.131.181'] -------------------------------------------
Command -> ['ls', '-l', '/mnt/efsdata'] -------------------------------------------
total 4
-rw-rw-r-- 1 1001 1001 0 Oct  5 02:51 169.254.131.181

暫くしてから実行すると別のIPでの書き込みされるのが確認できると思います。

Command -> ['touch', '/mnt/efsdata/169.254.171.253'] -------------------------------------------
Command -> ['ls', '-l', '/mnt/efsdata'] -------------------------------------------
total 8
-rw-rw-r-- 1 1001 1001 0 Oct  5 02:51 169.254.131.181
-rw-rw-r-- 1 1001 1001 0 Oct  5 03:04 169.254.171.253