AWS CDK(TS)でECSにEFSをmountさせるのにめちゃはまったのでメモとして残しておく

# AWS CDK で Qdrant を ECS にデプロイし、EFS をマウントしたときのメモ

AWS CDK を使って [Qdrant](https://qdrant.tech/)（ベクターデータベース）を Fargate/ECS 上で動かしつつ、データの永続化に EFS を使おうとした際に色々とはまったので、その時のメモです。  
特に以下の記事にある Tips がとても参考になり、助かりました。

- [ECS タスクに EFS をマウントする際の TIPS](https://qiita.com/ren8k/items/3724c4dd8e519e8e3bf0#ecs-%E3%82%BF%E3%82%B9%E3%82%AF%E3%81%AB-efs-%E3%82%92%E3%83%9E%E3%82%A6%E3%83%B3%E3%83%88%E3%81%99%E3%82%8B%E9%9A%9B%E3%81%AE-tips)

ChatGPT などの LLM に聞きながら進めていたら、逆に色々な設定を試しすぎてしまい、不要なものまで入っている可能性があります。そのため、このコードは「たぶんいらないものもあるけれど、一応動いた形」という前提でご参照ください。

---

## やりたいこと

- Qdrant というコンテナイメージを Fargate/ECS にデプロイ
- 保存したベクターやメタデータを EFS に永続化
- Fargate タスクから EFS をマウントし、`/qdrant/storage` ディレクトリとして使う

---

## はまったポイント

1. **FileSystem の availabilityZones 設定の付け忘れ**  
   - EFS をプライベートサブネットに作成する際、`vpcSubnets` で `availabilityZones` を指定しないとデプロイでエラーになることがありました。  
2. **FileSystem の `grantReadWrite` 設定の付け忘れ**  
   - ECS タスクロールなど、書き込みを行うロールに明示的に EFS へのアクセス権を付与していなかったためアクセス拒否。  
3. **ECS タスクの `authorizationConfig` で `iam: true` を設定し忘れ**  
   - `iam: 'ENABLED'` にしないと IAM による認可が働かず、EFS のマウントが失敗。
---

## 実際の CDK コード例

以下に今回動いたコードをそのまま載せます。  
（※すでに述べたように、余計なものや不要なものが混ざっているかもしれませんが、必要最低限の実装部分も含まれているはずです。）

```typescript
import * as cdk from 'aws-cdk-lib';
import { Construct } from 'constructs';
import * as ec2 from 'aws-cdk-lib/aws-ec2';
import * as efs from 'aws-cdk-lib/aws-efs';
import * as ecs from 'aws-cdk-lib/aws-ecs';
import * as iam from 'aws-cdk-lib/aws-iam';
import * as ecr from 'aws-cdk-lib/aws-ecr';

interface MyStackProps extends cdk.StackProps {
  stackName: string;
  qdrantSG: ec2.SecurityGroup;
}

export class MyStack extends cdk.Stack {
  constructor(scope: Construct, id: string, props: MyStackProps) {
    super(scope, id, props);

    // （VPC を新規に作成する場合）
    const vpc = new ec2.Vpc(this, "VPC", {
      availabilityZones: ['ap-northeast-1a', 'ap-northeast-1c'],
      natGateways: 1,
      subnetConfiguration: [
        {
          name: 'Public',
          subnetType: ec2.SubnetType.PUBLIC,
          cidrMask: 24
        },
        {
          name: 'Private',
          subnetType: ec2.SubnetType.PRIVATE_WITH_EGRESS,
          cidrMask: 24
        }
      ],
      enableDnsHostnames: true,
      enableDnsSupport: true
    });

    // ECS Task の実行ロール
    const taskExecutionRole = new iam.Role(this, "ecsTaskExecutionRole", {
      roleName: `${props.stackName}-ecsTaskExecutionRole`,
      assumedBy: new iam.ServicePrincipal("ecs-tasks.amazonaws.com")
    });

    // タスク実行ロールに ECR 関連のアクセス権限を付与
    taskExecutionRole.addToPolicy(
      new iam.PolicyStatement({
        effect: iam.Effect.ALLOW,
        actions: [
          "ecr:GetAuthorizationToken",
          "ecr:BatchCheckLayerAvailability",
          "ecr:GetDownloadUrlForLayer",
          "ecr:BatchGetImage"
        ],
        resources: ["*"]
      })
    );
    
    // タスク実行時の IAM ロール
    const taskRole = new iam.Role(this, "ecsTaskRole", {
      assumedBy: new iam.ServicePrincipal("ecs-tasks.amazonaws.com"),
      roleName: `${props.stackName}-ecsTaskRole`
    });

    // SSM / SecretsManager へのアクセス例
    taskRole.addManagedPolicy(
      iam.ManagedPolicy.fromAwsManagedPolicyName("AmazonSSMManagedInstanceCore")
    );
    taskRole.addManagedPolicy(
      iam.ManagedPolicy.fromAwsManagedPolicyName("SecretsManagerReadWrite")
    );

    // EFS の作成
    const fileSystem = new efs.FileSystem(this, 'QdrantStorage', {
      vpc: vpc,
      lifecyclePolicy: efs.LifecyclePolicy.AFTER_14_DAYS,
      performanceMode: efs.PerformanceMode.GENERAL_PURPOSE,
      removalPolicy: cdk.RemovalPolicy.DESTROY,
      vpcSubnets: {
        subnetType: ec2.SubnetType.PRIVATE_WITH_EGRESS,
        availabilityZones: vpc.availabilityZones
      }
    });

    // タスクロールに EFS への Read/Write 権限を付与
    fileSystem.grantReadWrite(taskRole);
    fileSystem.grantReadWrite(taskExecutionRole);

    // ECS の FargateTaskDefinition を作成
    const qdrantTaskDef = new ecs.FargateTaskDefinition(this, 'QdrantTaskDef', {
      memoryLimitMiB: 512,
      cpu: 256,
      taskRole: taskRole,
      executionRole: taskExecutionRole,
      family: `${props.stackName}-Qdrant`,
      volumes: [
        {
          name: 'qdrant_data',
          efsVolumeConfiguration: {
            fileSystemId: fileSystem.fileSystemId,
            transitEncryption: 'ENABLED',
            authorizationConfig: {
              iam: 'ENABLED'
            },
          }
        }
      ],
    });

    // Qdrant の ECR リポジトリを参照してコンテナを作成
    const qdrantRepo = ecr.Repository.fromRepositoryName(this, 'QdrantRepo', 'qdrant');
    const qdrantContainer = qdrantTaskDef.addContainer('QdrantContainer', {
      image: ecs.ContainerImage.fromRegistry('qdrant/qdrant:v1.8.1'),
      portMappings: [{ containerPort: 6333 }],
      environment: {
        TZ: "Asia/Tokyo"
      },
    });

    // コンテナ内の /qdrant/storage を EFS ボリュームとマウント
    qdrantContainer.addMountPoints({
      sourceVolume: 'qdrant_data',
      containerPath: '/qdrant/storage',
      readOnly: false
    });

    // サービスを作成
    // 例としてすでに作成済みの ECS クラスターを渡している想定
    const cluster = ecs.Cluster.fromClusterAttributes(this, 'ImportedCluster', {
      clusterName: 'MyCluster',
      vpc: vpc,
      securityGroups: [] // 必要に応じて
    });

    // Service 作成
    const qdrantService = new ecs.FargateService(this, 'QdrantService', {
      serviceName: `${props.stackName}-Qdrant`,
      cluster,
      taskDefinition: qdrantTaskDef,
      desiredCount: 1,
      assignPublicIp: false,
      vpcSubnets: { subnetType: ec2.SubnetType.PRIVATE_WITH_EGRESS },
      securityGroups: [props.qdrantSG],
      cloudMapOptions: {
        name: 'qdrant',
        cloudMapNamespace: {
          // こちらも別途作成・参照する必要あり
          namespaceArn: 'arn:aws:servicediscovery:xxx:namespace/xxx',
          namespaceId: 'xxx',
          type: ecs.CloudMapNamespaceType.HTTP
        }
      },
      platformVersion: ecs.FargatePlatformVersion.LATEST,
    });

    // EFS のデフォルトポート(2049)を、Qdrant Service から許可
    fileSystem.connections.allowDefaultPortFrom(qdrantService, "Allow Qdrant to access EFS");
  }
}
```

---

## まとめ

- EFS を ECS タスクにマウントする際は、**FileSystem 側の設定**と**ECS タスクのボリューム設定**の両方をしっかり行う必要があります。
- 特に **`iam: 'ENABLED'`** を入れないといけない点と、**`grantReadWrite`** で適切に権限を付与しないとアクセス拒否が起きる点は要注意。
- 今回は [こちらの記事](https://qiita.com/ren8k/items/3724c4dd8e519e8e3bf0#ecs-%E3%82%BF%E3%82%B9%E3%82%AF%E3%81%AB-efs-%E3%82%92%E3%83%9E%E3%82%A6%E3%83%B3%E3%83%88%E3%81%99%E3%82%8B%E9%9A%9B%E3%81%AE-tips) が非常に参考になりました。
- LLM (ChatGPT) で答えをもらおうとすると、様々なサンプルコードが降ってきて、**かえって設定が複雑になりがち**なので、公式ドキュメントや信頼できる記事をしっかり読んで設定を確認するのがやはり大事と感じました。

「とりあえず動く」実装にはなったものの、まだ不要なものが含まれている可能性がありますが、何かの参考になれば幸いです。もし整理してもっとスマートな設定にした方が良い場合は、この記事を参考にしつつ適宜削ぎ落としてみてください。

---

以上、AWS CDK を使って Qdrant + ECS + EFS を構成するときのメモでした。