[AWS CloudFormation] VPC Lambda & EFS定義

# 目的

LambdaからEFS（Amazon Elastic File System）を使用するためのCloudFormationテンプレートを作成する
VPC内にLambdaを配置する必要があるため、以下の定義を行う

- VPC関連リソース
- EFS関連リソース
- Lambda（SQSトリガ）
- VPC LambdaからAWSリソースへアクセスするためのVPCエンドポイント

# テンプレート

```template.yaml
  vpcSample:
    Type: AWS::EC2::VPC
    Properties:
      CidrBlock: 10.0.0.0/16
      EnableDnsSupport: true
      EnableDnsHostnames: true
      InstanceTenancy: default
      Tags: 
        - Key: Name   # タグで名前を設定する
          Value: !Sub ${EnvName}-vpcSample

  subnetSample1:
    Type: AWS::EC2::Subnet
    Properties:
      AvailabilityZone:
        Fn::Select:
          - 0  # 使用可能なAZリストのindex0を使用
          - !GetAZs   # 使用可能なAZリスト取得
            Ref: AWS::Region
      CidrBlock: 10.0.0.0/24
      VpcId: !Ref vpcSample
      Tags: 
        - Key: Name   # タグで名前を設定する
          Value: !Sub ${EnvName}-subnetSample1

  subnetSample2:
    Type: AWS::EC2::Subnet
    Properties:
      AvailabilityZone:
        Fn::Select:
          - 1
          - !GetAZs 
            Ref: AWS::Region
      CidrBlock: 10.0.1.0/24
      VpcId: !Ref vpcSample
      Tags: 
        - Key: Name
          Value: !Sub ${EnvName}-subnetSample2

  # VPC作成時に自動作成されるメインルートテーブルは、
  # cfnから参照できないためルートテーブルを作成
  routeTableSample:
    Type: AWS::EC2::RouteTable
    Properties:
      VpcId: !Ref vpcSample

  # ルートテーブルとサブネットの関連付け
  subnetRouteTableSample1:
    Type: AWS::EC2::SubnetRouteTableAssociation
    Properties:
      RouteTableId: !Ref routeTableSample
      SubnetId: !Ref subnetSample1

  subnetRouteTableSample2:
    Type: AWS::EC2::SubnetRouteTableAssociation
    Properties:
      RouteTableId: !Ref routeTableSample
      SubnetId: !Ref subnetSample2

  # VPC LambdaからS3にアクセスするためのVPCエンドポイント
  vpcEndpointS3Sample:
    Type: AWS::EC2::VPCEndpoint
    Properties:
      VpcId: !Ref vpcSample
      VpcEndpointType: Gateway
      ServiceName: !Sub com.amazonaws.${AWS::Region}.s3
      RouteTableIds:
        - !Ref routeTableSample

  # VPC LambdaからDynamoDBにアクセスするためのVPCエンドポイント
  vpcEndpointDynamodbSample:
    Type: AWS::EC2::VPCEndpoint
    Properties:
      VpcId: !Ref vpcSample
      VpcEndpointType: Gateway
      ServiceName: !Sub com.amazonaws.${AWS::Region}.dynamodb
      RouteTableIds:
        - !Ref routeTableSample

  # VPC LambdaからSQSにアクセスするためのVPCエンドポイント
  vpcEndpointSqsSample:
    Type: AWS::EC2::VPCEndpoint
    Properties:
      VpcId: !Ref vpcSample
      VpcEndpointType: Interface  # Gatewayタイプが対応しているのはS3, DynamoDBのみ
      PrivateDnsEnabled: true
      ServiceName: !Sub com.amazonaws.${AWS::Region}.sqs
      SubnetIds: 
        - !Ref subnetSample1
        - !Ref subnetSample2
      SecurityGroupIds:
        - !GetAtt securityGroupSampleSQS.GroupId

  # Lambdaが使用するセキュリティグループ
  securityGroupSampleLambda:
    Type: AWS::EC2::SecurityGroup
    Properties:
      GroupName: !Sub ${EnvName}-securityGroupSampleLambda
      GroupDescription: for lambda
      VpcId: !Ref vpcSample
      SecurityGroupEgress:
        - CidrIp: 0.0.0.0/0
          IpProtocol: -1

  # EFSに設定するセキュリティグループ
  securityGroupSampleEFS:
    Type: AWS::EC2::SecurityGroup
    Properties:
      GroupName: !Sub ${EnvName}-securityGroupSampleEFS
      GroupDescription: for efs
      VpcId: !Ref vpcSample
      SecurityGroupEgress:
        - CidrIp: 0.0.0.0/0
          IpProtocol: -1
      SecurityGroupIngress:
        - IpProtocol: tcp
          SourceSecurityGroupId: !GetAtt securityGroupSampleLambda.GroupId  # Lambdaが使用するsgを通す
          FromPort: 2049 # NFSに使用するポート番号
          ToPort: 2049

  # SQS用のVPCエンドポイントに設定するセキュリティグループ
  securityGroupSampleSQS:
    Type: AWS::EC2::SecurityGroup
    Properties:
      GroupName: !Sub ${EnvName}-securityGroupSampleSQS
      GroupDescription: for sqs
      VpcId: !Ref vpcSample
      SecurityGroupEgress:
        - CidrIp: 0.0.0.0/0
          IpProtocol: -1
      SecurityGroupIngress:
        - IpProtocol: tcp
          SourceSecurityGroupId: !GetAtt securityGroupSampleLambda.GroupId  # Lambdaが使用するsgを通す
          FromPort: 443 # HTTPSに使用するポート番号
          ToPort: 443

  efsFileSystemSample:
    Type: AWS::EFS::FileSystem
    Properties:
      BackupPolicy:
        Status: ENABLED
      PerformanceMode: generalPurpose
      Encrypted: true
      LifecyclePolicies:
        - TransitionToIA: AFTER_30_DAYS
        - TransitionToPrimaryStorageClass: AFTER_1_ACCESS
      FileSystemTags: 
        - Key: Name   # タグで名前を設定する
          Value: !Sub ${EnvName}-efsFileSystemSample

  efsMountTargetSample1:
    Type: AWS::EFS::MountTarget
    Properties:
      FileSystemId: !Ref efsFileSystemSample
      SubnetId: !Ref subnetSample1
      SecurityGroups:
        - !GetAtt securityGroupSampleEFS.GroupId

  efsMountTargetSample2:
    Type: AWS::EFS::MountTarget
    Properties:
      FileSystemId: !Ref efsFileSystemSample
      SubnetId: !Ref subnetSample2
      SecurityGroups:
        - !GetAtt securityGroupSampleEFS.GroupId

  efsAccessPointSample:
    Type: AWS::EFS::AccessPoint
    Properties:
      FileSystemId: !Ref efsFileSystemSample
      PosixUser:  # このアクセスポイントを使用するLambdaが使用するユーザ/グループID
        Uid: 1001
        Gid: 1001
      RootDirectory:
        CreationInfo: # ディレクトリ作成者のユーザ/グループID
          OwnerGid: 1001
          OwnerUid: 1001
          Permissions: 755 # パーミッション
        Path: /my-function  # ファイルシステム上に生成されるディレクトリ

  SampleLambdaRole:
    Type: AWS::IAM::Role
    Properties:
      RoleName: SampleLambdaRole
      Path: /
      MaxSessionDuration: 3600
      AssumeRolePolicyDocument:
        Version: 2012-10-17
        Statement:
          - Effect: Allow
            Principal:
              Service: lambda.amazonaws.com
            Action: sts:AssumeRole
      Policies:
        - PolicyName: SampleVpcAccess
          PolicyDocument:
            Version: 2012-10-17
            Statement:
              - Effect: Allow
                Resource: [
                  '*'
                ]
                Action: [
                  ec2:CreateNetworkInterface,
                  ec2:DescribeNetworkInterfaces,
                  ec2:DeleteNetworkInterface,
                  ec2:AssignPrivateIpAddresses,
                  ec2:UnassignPrivateIpAddresses
                ]
        - PolicyName: SampleEfsAccess
          PolicyDocument:
            Version: 2012-10-17
            Statement:
              - Effect: Allow
                Resource: [
                  '*'
                ]
                Action: [
                  elasticfilesystem:ClientMount,
                  elasticfilesystem:ClientWrite,
                  elasticfilesystem:DescribeMountTargets
                ]

  sqsSampleLambda:
    Type: AWS::Serverless::Function
    DependsOn: 
      - efsMountTargetSample1   # LambdaからEFS使用に必須
      - efsMountTargetSample2
    Properties:
      Handler: main.lambda_handler
      Runtime: python3.7
      Role: !Sub arn:aws:iam::${AWS::AccountId}:role/SampleLambdaRole
      # ...省略
      Environment:
        Variables:
          DNS_NAME: !Sub https://sqs.${AWS::Region}.amazonaws.com   # VPC LambdaからSQS使用の際に必要
      Events:
        sqsSampleQueueEvent:
          Type: SQS   # LambdaのトリガとしてSQSを使用するのみであればSQS用のVPCエンドポイントは不要
          Properties:
            Queue:
              Fn::GetAtt:
                - sqsSampleQueue
                - Arn
            BatchSize: 1
      VpcConfig:  # VPC設定
        SecurityGroupIds:
          - !GetAtt securityGroupSampleLambda.GroupId
        SubnetIds:
          - !Ref subnetSample1
          - !Ref subnetSample2
      FileSystemConfigs:  # EFS設定
        - Arn: !GetAtt efsAccessPointSample.Arn
          LocalMountPath: /mnt/efs  # /mnt/efs にアクセスポイントパス /my-function がマウントされる

  sqsSampleQueue:
    Type: AWS::SQS::Queue
    Properties:
      DelaySeconds: 0
      KmsMasterKeyId: alias/aws/sqs
      # ...省略
```

# EFSアクセスポイント定義内容

EFSアクセスポイントには以下の設定を行う

- ディレクトリパス
- ユーザID、グループID（このアクセスポイントを使用してアクセスした者に割り当たるID）
- ディレクトリ所有者のユーザID、グループID
- パーミッション

上記テンプレートは、
ユーザID、グループID（Lambdaに割り当てるID）とディレクトリ所有者のユーザID、グループIDと同じとし、
Lambdaはディレクトリ所有者としてアクセスするよう設定している
パーミッションは755とし、ディレクトリ所有者は読み書き可能にしている

# EFSアクセスポイントの注意点

アクセスポイントを作成すると、
EFS上に指定した属性（ディレクトリ所有者のユーザID、グループID、パーミッション）でディレクトリが作成される
このディレクトリは、アクセスポイントの削除や、同一ディレクトリパスでアクセスポイントを追加しても削除/更新されない

# VPC LambdaからAWSリソースアクセス時の注意点

VPC Lambdaからboto3でAWSリソースアクセス時、以下のように`endpoint_url`を指定する必要があるものがある
このオプションがない場合、アクセスタイムアウトが発生する

```template.yaml
      Environment:
        Variables:
          DNS_NAME: !Sub https://sqs.${AWS::Region}.amazonaws.com   # VPC LambdaからSQS使用の際に必要
```
```handler.py
sqs_dns_name = os.environ.get('SQS_DNS_NAME')
sqs = boto3.resource('sqs', endpoint_url=sqs_dns_name)
```

SQS以外にSTSも同様に`endpoint_url`が必要だったが、すべてのAWSリソースアクセスに必要なわけではない
（S3は不要だった）

```python
client = boto3.client('sts', endpoint_url=dns_name)
```