EFSをECS Fargateにマウントする定義をCFnで書きながら理解する

## はじめに

2020年4月にAWS Fargateのプラットフォームバージョン1.4.0がリリースされ、それに伴い、ECS FargateからAmazon Elastic Filesystem（以下、EFS）が利用できるようになりました。今回これを必要とする要件があり、EFSを初めて利用することになりました。そのため、基本的な概念を整理するために記事にしました。
今回の進め方として、CloudFormation（以下、CFn）で設定が必要な要素を確認しつつ、不明点があればドキュメントを読んで理解していったので、本記事でもCFnの定義をみながらEFSの要素についてみていきたいと思います。

## 最終的な構成

これから順を追って各コンポーネントについてみていきますが、今回の最終的な構成としては以下のようになります。
![EFS_ECSFargate-Page-1 (1).png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/316261/46628445-4886-9331-d1de-bd854a4ba502.png)

サブネットは二つで、それぞれのサブネットにECS Fargate上でタスクが稼働しています。そして、それぞれのタスクからEFSをマウントするという構成です。

## CFnの定義
### 1. ECSの作成

まずはECS FargateをCloudFormationで定義していきます。構成図でいうと以下の赤枠の部分になります。
![EFS_ECSFargate-Page-1 (3).png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/316261/5f4077ad-a595-76e1-738d-a28d108d8a73.png)

CloudFormationでは以下のようになります。この段階ではEFSに絡む定義はまだ出てきません。
尚、今回直接関連しないパラメータやサブネットなどの一部のリソースについては省略させていただいております。

```yaml

  ECSService:
    Type: 'AWS::ECS::Service'
    Properties:
      Cluster: !Ref ECSCluster
      DesiredCount: 2
      LaunchType: 'FARGATE'
      LoadBalancers:
        - ContainerName: 'api'
          ContainerPort: 80
          TargetGroupArn: !Ref ElasticLoadBalancingV2TargetGroupExternal
      NetworkConfiguration:
        AwsvpcConfiguration:
          AssignPublicIp: 'DISABLED'
          SecurityGroups:
            - !Ref EC2SecurityGroupAPI
          Subnets:
            - !Ref EC2SubnetPrivateAppAZ1
            - !Ref EC2SubnetPrivateAppAZ2
      PlatformVersion: '1.4.0'
      TaskDefinition: !Ref ECSTaskDefinition

  ECSTaskDefinition:
    Type: 'AWS::ECS::TaskDefinition'
    Properties:
      Family: efs-test
      RequiresCompatibilities:
        - 'FARGATE'
      Cpu: 1024
      Memory: 2048
      NetworkMode: 'awsvpc'
      ExecutionRoleArn: !GetAtt IAMRoleECSTaskExecution.Arn
      TaskRoleArn: !GetAtt IAMRoleAPI.Arn
      ContainerDefinitions:
        - Image: !Sub ${AWS::AccountId}.dkr.ecr.${AWS::Region}.amazonaws.com/${ECRRepositoryAPI}:latest
          Name: 'api'
          Cpu: 512
          MemoryReservation: 1024
            - ContainerPort: 8080
              HostPort: 80
              Protocol: 'tcp'
          LogConfiguration:
            LogDriver: 'awslogs'
            Options:
              awslogs-group: '/ecs/efs-test'
              awslogs-region: !Ref AWS::Region
              awslogs-stream-prefix: 'ecs'
              awslogs-create-group: true
          Essential: true

  IAMRoleAPI:
    Type: 'AWS::IAM::Role'
    Properties:
      AssumeRolePolicyDocument:
        Version: '2012-10-17'
        Statement:
        - Effect: 'Allow'
          Principal:
            Service: 'ecs-tasks.amazonaws.com'
          Action: 'sts:AssumeRole'
      ManagedPolicyArns:
        - 'arn:aws:iam::aws:policy/AWSOpsWorksCloudWatchLogs'

```


### 2. EFS Filesystemの作成

次にEFSファイルシステムを作成していきます。構成図では以下の箇所になります。
![EFS_ECSFargate-Page-1 (2).png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/316261/7104bc7c-e61c-93d5-3076-e0edcdaacd45.png)

ここではバックアップや暗号化の有無、パフォーマンスモード、スループットモードを設定していきます。

```yaml

  EFSFileSystem:
    Type: 'AWS::EFS::FileSystem'
    Properties:
      BackupPolicy:
        Status: 'ENABLED'
      Encrypted: true
      FileSystemTags:
        - Key: 'Name'
          Value: !Sub 'efs-filesystem'
      FileSystemPolicy:
        Version: '2012-10-17'
        Statement:
          - Effect: 'Allow'
            Action:
              - 'elasticfilesystem:ClientWrite'
              - 'elasticfilesystem:ClientMount'
            Principal:
              AWS: !GetAtt IAMRoleAPI.Arn
      LifecyclePolicies:
        - TransitionToIA: AFTER_30_DAYS 
      PerformanceMode: 'generalPurpose'
      ThroughputMode: 'bursting'
```

注意点としては、パフォーマンスモードはgeneralPurpose、maxIOから選択可能なのですが、ファイルシステムの作成後は変更できない点です。一般的なファイルサービスとして利用する場合はgeneralPurpose、ビッグデータ解析などのワークロードの場合にmaxIOを選択すると良いと思います。

スループットモードはバーストかプロビジョニングを選択できます。バーストモードは短期間で高いスループットが必要になり、残りの時間は低いスループットで良いワークロードが想定されています。
バーストモードはベースラインとクレジットという概念を抑えておく必要があります。EFSのスループットにはベースラインが設定されており、これを超えるスループットを必要とする場合、クレジットを消費してスループットをバーストすることができます。クレジットを使い切った場合は当然バーストすることはできないため、ベースラインでは不足している場合でもベースラインのスループットのままとなります。クレジットは時間と共に蓄積されていきます。ベースラインのスループットはディスク容量を増やすことであげることができます。

バーストモードではクレジットが常に枯渇するようなワークロードの場合はプロビジョニングを検討します。プロビジョニングは必要なスループットを指定することで、そのスループットを維持できます。ただし、ベースラインのスループットを超えてプロビジョニングされたスループット分に別途課金が発生します。ここで発生する課金は安くないため注意が必要です。

EFSのパフォーマンスについては[こちら](https://docs.aws.amazon.com/ja_jp/efs/latest/ug/performance.html)のドキュメントにまとめられています。

また、ここで重要な設定としてFileSystemPolicyがあります。これはこのファイルシステムに対してリソースベースのアクセスを制御することが可能になります。今回はECS FargateのIAMRoleからマウント、読み込み、書き込みのアクセスを許可しています。


### 3. EFS MountTargetの作成

次にEFS マウントターゲットを作成していきます。構成図では以下の箇所になります。
![EFS_ECSFargate-Page-1 (4).png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/316261/aa611874-b3ab-f220-464b-7874f9a6f71a.png)

マウントターゲットはEFSにアクセスためのリソースになります。これは耐障害性の観点からAZに一つずつ作成することが推奨されています。AZごとに作成できるマウントターゲットは一つだけのため、一つのAZに複数のサブネットが含まれる場合もマウントターゲットを作成するサブネットを一つ選択して作成します。他のサブネットからは同じAZ内のサブネットに作られたマウントターゲットを利用できます。
また、マウントターゲットにはセキュリティグループをアタッチできるため、EFSのファイアウォールとしての役割も担います。セキュリティグループはport 2049からのアクセスを許可することでアクセス可能になります。

```yaml
  EFSMountTargetPrivateAppAZ1:
    Type: 'AWS::EFS::MountTarget'
    Properties: 
      FileSystemId: !Ref EFSFileSystem
      SubnetId: !Ref EC2SubnetPrivateAppAZ1
      SecurityGroups: 
        - !Ref EC2SecurityGroupEFS

  EFSMountTargetPrivateAppAZ2:
    Type: 'AWS::EFS::MountTarget'
    Properties: 
      FileSystemId: !Ref EFSFileSystem
      SubnetId: !Ref EC2SubnetPrivateAppAZ2
      SecurityGroups: 
        - !Ref EC2SecurityGroupEFS

  EC2SecurityGroupEFS:
    Type: 'AWS::EC2::SecurityGroup'
    Properties:
      GroupDescription: 'efs-filesystem securitygroup'
      SecurityGroupIngress:
        - IpProtocol: tcp
          FromPort: 2049
          ToPort: 2049
          SourceSecurityGroupId: !Ref EC2SecurityGroupAPI
      Tags:
        - Key: 'Name'
          Value: 'efs-filesystem'
      VpcId: !Ref EC2VPC
```

### 4. EFS AccessPointの作成

次にアクセスポイントを作成していきます。構成図では以下の箇所になります。
![EFS_ECSFargate-Page-1 (6).png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/316261/d87a6c11-3f9c-1e4a-acd6-2c9aa3ec110d.png)

CloudFormationの定義としては以下のようになります。

```yaml
  EFSAccessPoint:
    Type: 'AWS::EFS::AccessPoint'
    Properties:
      FileSystemId: !Ref EFSFileSystem
      PosixUser:
        Uid: "1001"
        Gid: "1001"
      RootDirectory:
        CreationInfo:
          OwnerGid: "1001"
          OwnerUid: "1001"
          Permissions: "0755"
        Path: "/mnt/efs"
```

私はこのアクセスポイントを間違って理解してしまい、少しハマりました。[ドキュメント](https://docs.aws.amazon.com/ja_jp/efs/latest/ug/efs-access-points.html)には以下のように記載されています。
> アクセスポイントを使用すると、アクセスポイントを介したすべてのファイルシステム要求に対してユーザー ID (ユーザーの POSIX グループなど) を適用できます。

~~ここでいう「適用」が指す意味を「適用するために上書き」してくれるものと勘違いし、それに沿った設定をしたところ、EFSファイルシステムにアクセスする際に `Permission Deny`が発生しました。挙動を確認したところ、おそらくここは「適用するために指定されたUID、GID以外を弾く」ことを指すようです。
そのため、NFSクライアント側でUID、GIDを設定する必要があります。ECSのタスク定義 or Dockerで特にUID、GIDを指定しない場合はデフォルトでrootユーザーでのアクセスとなるため、同じく`Permission Deny` が発生します。回避策としてはファイルシステムポリシーに「elasticfilesystem:ClientRootAccess」を設定する方法があります。ただしこれはno_root_squashを設定するのと同等のリスクが生まれるため、リスクを考慮して設定するか判断する必要があります。~~

すみません、ここはPosixUserで指定したUID、GIDに上書きしてくれる理解であっていました。改めて検証したところ、ECSのタスク定義やDockerfile内でUserを指定することなく、正常にファイル書き込みされることを確認しました。


### 5. ECS FargateからEFSをマウントする設定

ここまででEFS側の設定は完了しているので、「 1. ECSの作成」で作成したECSのタスク定義を編集し、EFSをマウントしていきます。
![EFS_ECSFargate-Page-1 (5).png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/316261/cc734919-abd1-5618-3787-f1943a2e0592.png)

設定箇所としては2箇所になります。
1. Volumeを定義してECSからEFSを認識させる設定
2. ボジュームをタスクからマウントする設定


```yaml
  ECSTaskDefinition:
    Type: 'AWS::ECS::TaskDefinition'
    Properties:
      Family: efs-test
      RequiresCompatibilities:
        - 'FARGATE'
      Cpu: 1024
      Memory: 2048
      NetworkMode: 'awsvpc'
      ExecutionRoleArn: !GetAtt IAMRoleECSTaskExecution.Arn
      TaskRoleArn: !GetAtt IAMRoleAPI.Arn
      #### ここを追加する。1.に該当する。
      Volumes:
        - Name: 'efs-filesystem'
          EFSVolumeConfiguration:
            AuthorizationConfig:
              AccessPointId: !Ref EFSAccessPoint
              IAM: 'ENABLED'
            FilesystemId: !Ref EFSFileSystem
            TransitEncryption: 'ENABLED'
      #### ここを追加する。1.に該当する。
      ContainerDefinitions:
        - Image: !Sub ${AWS::AccountId}.dkr.ecr.${AWS::Region}.amazonaws.com/${ECRRepositoryAPI}:latest
          Name: 'api'
          Cpu: 512
          MemoryReservation: 1024
            - ContainerPort: 8080
              HostPort: 80
              Protocol: 'tcp'
          LogConfiguration:
            LogDriver: 'awslogs'
            Options:
              awslogs-group: '/ecs/efs-test'
              awslogs-region: !Ref AWS::Region
              awslogs-stream-prefix: 'ecs'
              awslogs-create-group: true
          #### ここを追加する。2.に該当する。
          MountPoints:
            -
              SourceVolume: 'efs-filesystem'
              ContainerPath: '/working'
          #### ここを追加する。2.に該当する。
          Essential: true
```

以上の設定により、ECS FargateからEFSを利用可能になります。

# 最後に

EFSを初めて使ってみて、アクセス制御の選択肢が豊富だなという感想を抱きました。
EFSをECSにマウントするCloudFormationの定義は意外と落ちていなかったりするので、参考になれば幸いです。