盆暗の学習記録

データサイエンス 、ソフトウェア開発、ビジネスについて日々学んだことの備忘録としていく予定です。初心者であり独学なので内容には誤りが含まれる可能性が大いにあります。

S3 Files + Lambdaの構成をSAMで作る

最近リリースされたAWS S3 Filesを触ってみました。AWS Serverless Application Model (AWS SAM) で試していたのですが、思ったより詰まるポイントが多かったのでコードを共有しておきます。

S3 Files とは

S3 Files は S3 バケットをファイルシステムとして EC2・Lambda・EKS・ECS にマウントして使えるようにするものです。

通常の S3 操作はSDK 経由(Pythonだと boto3.client('s3').get_object(...) など)で行いますが、S3 Files を使うと open('data.csv') のような通常のファイル操作で S3 のデータにアクセスできます。SDKの実装部分が不要になって開発が楽になるというわけです。

aws.amazon.com

SAMで作る

S3 Filesを使うには

  • LambdaをマウントするVPC
  • TCP 2049で通信するセキュリティグループ
  • ファイルシステムからバケットにアクセスするためのIAMロール

など、色々と設定する必要があります。

docs.aws.amazon.com

またSAM特有?の問題として、1つのCloudFormation Stackでまとめてデプロイしようとしたところ、AWS::S3Files::MountTarget が作成完了したもの利用可能なステータスになる前にLambdaのリソースを作り始めてしまってエラーになりました。(エラーメッセージ:"mount targets created in all availability zones the function will execute in, but not all are in the available life cycle state yet. Please wait for them to become available and try the request again.")(現時点で最新のSAM CLI ver.1.158.0の問題です)

そこで

  1. S3 FilesやVPCなどの作成用のStack
  2. LambdaのStack

に分けて、時間を空けて作ることでうまくデプロイできるようになりました。

S3 Filesなどを作るStackのtemplate.yaml

AWSTemplateFormatVersion: "2010-09-09"
Transform: AWS::Serverless-2016-10-31

Resources:
  # S3 Files を利用するための VPC とサブネットを定義
  VPC:
    Type: AWS::EC2::VPC
    Properties:
      CidrBlock: 10.0.0.0/16
      EnableDnsHostnames: true
      EnableDnsSupport: true

  PrivateSubnet:
    Type: AWS::EC2::Subnet
    Properties:
      VpcId: !Ref VPC
      CidrBlock: 10.0.1.0/24
      AvailabilityZone: !Select [0, !GetAZs ""]

  # TCP 2049で通信するセキュリティグループ(Lambda用とS3Files MountTarget用)
  LambdaSecurityGroup:
    Type: AWS::EC2::SecurityGroup
    Properties:
      GroupDescription: Security group for Lambda
      VpcId: !Ref VPC
 
  MountTargetSecurityGroup:
    Type: AWS::EC2::SecurityGroup
    Properties:
      GroupDescription: Security group for S3 Files mount target
      VpcId: !Ref VPC

  LambdaToMountTargetEgress:
    Type: AWS::EC2::SecurityGroupEgress
    Properties:
      GroupId: !GetAtt LambdaSecurityGroup.GroupId
      Description: Allow NFS to S3 Files mount target
      IpProtocol: tcp
      FromPort: 2049
      ToPort: 2049
      DestinationSecurityGroupId: !GetAtt MountTargetSecurityGroup.GroupId

  MountTargetFromLambdaIngress:
    Type: AWS::EC2::SecurityGroupIngress
    Properties:
      GroupId: !GetAtt MountTargetSecurityGroup.GroupId
      Description: Allow NFS from Lambda
      IpProtocol: tcp
      FromPort: 2049
      ToPort: 2049
      SourceSecurityGroupId: !GetAtt LambdaSecurityGroup.GroupId


  # ファイルシステムからバケットにアクセスするためのIAMロール
  # 参考: https://docs.aws.amazon.com/AmazonS3/latest/userguide/s3-files-prereq-policies.html#s3-files-prereq-iam
  S3AccessRole:
    Type: AWS::IAM::Role
    Properties:
      AssumeRolePolicyDocument:
        Version: "2012-10-17"
        Statement:
          - Sid: AllowS3FilesAssumeRole
            Effect: Allow
            Principal:
              Service: elasticfilesystem.amazonaws.com
            Action: sts:AssumeRole
            Condition:
              StringEquals:
                aws:SourceAccount: !Ref AWS::AccountId
              ArnLike:
                aws:SourceArn: !Sub "arn:aws:s3files:${AWS::Region}:${AWS::AccountId}:file-system/*"
      Policies:
        - PolicyName: S3AccessPolicy
          PolicyDocument:
            Version: "2012-10-17"
            Statement:
              - Sid: S3BucketPermissions
                Effect: Allow
                Action:
                  - s3:ListBucket
                  - s3:ListBucketVersions
                Resource: !Sub "arn:aws:s3:::${S3Bucket}"
                Condition:
                  StringEquals:
                    aws:ResourceAccount: !Ref AWS::AccountId

              - Sid: S3ObjectPermissions
                Effect: Allow
                Action:
                  - s3:AbortMultipartUpload
                  - s3:DeleteObject*
                  - s3:GetObject*
                  - s3:List*
                  - s3:PutObject*
                Resource: !Sub "arn:aws:s3:::${S3Bucket}/*"
                Condition:
                  StringEquals:
                    aws:ResourceAccount: !Ref AWS::AccountId

              - Sid: EventBridgeManage
                Effect: Allow
                Action:
                  - events:DeleteRule
                  - events:DisableRule
                  - events:EnableRule
                  - events:PutRule
                  - events:PutTargets
                  - events:RemoveTargets
                Resource: "arn:aws:events:*:*:rule/DO-NOT-DELETE-S3-Files*"
                Condition:
                  StringEquals:
                    events:ManagedBy: elasticfilesystem.amazonaws.com

              - Sid: EventBridgeRead
                Effect: Allow
                Action:
                  - events:DescribeRule
                  - events:ListRuleNamesByTarget
                  - events:ListRules
                  - events:ListTargetsByRule
                Resource: "arn:aws:events:*:*:rule/*"

  S3Bucket:
    Type: AWS::S3::Bucket
    Properties:
      BucketName: !Sub "${AWS::AccountId}-s3-files-example"
      VersioningConfiguration:
        Status: Enabled
      BucketEncryption:
        ServerSideEncryptionConfiguration:
          - ServerSideEncryptionByDefault:
              SSEAlgorithm: AES256

  S3Files:
    Type: AWS::S3Files::FileSystem
    Properties:
      Bucket: !GetAtt S3Bucket.Arn
      RoleArn: !GetAtt S3AccessRole.Arn
      SynchronizationConfiguration:
        ExpirationDataRules:
          # キャッシュされたデータが期限切れになるまでの日数
          - DaysAfterLastAccess: 1
        ImportDataRules:
          - Prefix: ""
            # SizeLessThan: 特定のサイズ未満のファイルをキャッシュ対象にする
            SizeLessThan: 1073741824 # 1GB
            Trigger: ON_DIRECTORY_FIRST_ACCESS

  S3FilesAccessPoint:
    Type: AWS::S3Files::AccessPoint
    Properties:
      FileSystemId: !GetAtt S3Files.FileSystemArn
      PosixUser:
        Uid: "1000"
        Gid: "1000"
      RootDirectory:
        Path: /accesspoint # /にするとPermission deniedで書き込めなかったため指定(S3FilesがマウントされたLambda内で/mnt/s3filesに保存するとS3上の/accesspointディレクトリに保存される)
        CreationPermissions:
          OwnerUid: "1000"
          OwnerGid: "1000"
          Permissions: "0755"

  S3FilesMountTarget:
    Type: AWS::S3Files::MountTarget
    Properties:
      FileSystemId: !Ref S3Files
      SubnetId: !Ref PrivateSubnet
      SecurityGroups:
        - !GetAtt MountTargetSecurityGroup.GroupId

  # Lambdaデプロイ用のStackで参照しやすくするためParameter storeに保存
  PrivateSubnetIdParam:
    Type: AWS::SSM::Parameter
    Properties:
      Name: /s3-files/PrivateSubnetId
      Type: String
      Value: !Ref PrivateSubnet

  S3FilesAccessPointArnParam:
    Type: AWS::SSM::Parameter
    Properties:
      Name: /s3-files/S3FilesAccessPointArn
      Type: String
      Value: !GetAtt S3FilesAccessPoint.AccessPointArn

  LambdaSecurityGroupIdParam:
    Type: AWS::SSM::Parameter
    Properties:
      Name: /s3-files/LambdaSecurityGroupId
      Type: String
      Value: !GetAtt LambdaSecurityGroup.GroupId

Lambdaを作るStackのtemplate.yaml

AWSTemplateFormatVersion: "2010-09-09"
Transform: AWS::Serverless-2016-10-31

Resources:
  LambdaFunction:
    Type: AWS::Serverless::Function
    Properties:
      FunctionName: LambdaFunction
      CodeUri: src/
      Handler: app.handler
      Runtime: python3.12
      Architectures:
        - x86_64
      Timeout: 60
      MemorySize: 1024
      Environment:
        Variables:
          S3_BUCKET_NAME: !Sub "${AWS::AccountId}-s3-files-example"
      Role: !GetAtt S3FilesRoleForFunction.Arn
      VpcConfig:
        SubnetIds:
          - "{{resolve:ssm:/s3-files/PrivateSubnetId}}"
        SecurityGroupIds:
          - "{{resolve:ssm:/s3-files/LambdaSecurityGroupId}}"
      FileSystemConfigs:
        - Arn: "{{resolve:ssm:/s3-files/S3FilesAccessPointArn}}"
          LocalMountPath: /mnt/s3files

  LambdaFunctionLogGroup:
    Type: AWS::Logs::LogGroup
    Properties:
      LogGroupName: !Sub "/aws/lambda/${LambdaFunction}"
      RetentionInDays: 14

  # S3 Files を Lambda から利用するための IAM ロールを定義
  # ref: https://docs.aws.amazon.com/AmazonS3/latest/userguide/s3-files-prereq-policies.html#s3-files-prereq-iam
  S3FilesRoleForFunction:
    Type: AWS::IAM::Role
    Properties:
      AssumeRolePolicyDocument:
        Version: "2012-10-17"
        Statement:
          - Effect: Allow
            Principal:
              Service: lambda.amazonaws.com
            Action: sts:AssumeRole
      ManagedPolicyArns:
        - arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole
        - arn:aws:iam::aws:policy/service-role/AWSLambdaVPCAccessExecutionRole
      Policies:
        - PolicyName: S3FilesClientPolicy
          PolicyDocument:
            Version: "2012-10-17"
            Statement:
              - Sid: S3FilesMount
                Effect: Allow
                Action:
                  - s3files:ClientMount
                  - s3files:ClientWrite
                Resource: "{{resolve:ssm:/s3-files/S3FilesAccessPointArn}}"
              - Sid: S3ReadWriteAccess
                Effect: Allow
                Action:
                  - s3:Get*
                  - s3:Put*
                Resource: !Sub "arn:aws:s3:::${AWS::AccountId}-s3-files-example/*"
              - Sid: S3BucketListAccess
                Effect: Allow
                Action:
                  - s3:ListBucket
                Resource: !Sub "arn:aws:s3:::${AWS::AccountId}-s3-files-example"