AWS CDK で Qdrant を ECS にデプロイし、EFS をマウントしたときのメモ
AWS CDK を使って Qdrant(ベクターデータベース)を Fargate/ECS 上で動かしつつ、データの永続化に EFS を使おうとした際に色々とはまったので、その時のメモです。
特に以下の記事にある Tips がとても参考になり、助かりました。
ChatGPT などの LLM に聞きながら進めていたら、逆に色々な設定を試しすぎてしまい、不要なものまで入っている可能性があります。そのため、このコードは「たぶんいらないものもあるけれど、一応動いた形」という前提でご参照ください。
やりたいこと
- Qdrant というコンテナイメージを Fargate/ECS にデプロイ
- 保存したベクターやメタデータを EFS に永続化
- Fargate タスクから EFS をマウントし、
/qdrant/storage
ディレクトリとして使う
はまったポイント
-
FileSystem の availabilityZones 設定の付け忘れ
- EFS をプライベートサブネットに作成する際、
vpcSubnets
でavailabilityZones
を指定しないとデプロイでエラーになることがありました。
- EFS をプライベートサブネットに作成する際、
-
FileSystem の
grantReadWrite
設定の付け忘れ- ECS タスクロールなど、書き込みを行うロールに明示的に EFS へのアクセス権を付与していなかったためアクセス拒否。
-
ECS タスクの
authorizationConfig
でiam: true
を設定し忘れ-
iam: 'ENABLED'
にしないと IAM による認可が働かず、EFS のマウントが失敗。
-
実際の CDK コード例
以下に今回動いたコードをそのまま載せます。
(※すでに述べたように、余計なものや不要なものが混ざっているかもしれませんが、必要最低限の実装部分も含まれているはずです。)
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
で適切に権限を付与しないとアクセス拒否が起きる点は要注意。 - 今回は こちらの記事 が非常に参考になりました。
- LLM (ChatGPT) で答えをもらおうとすると、様々なサンプルコードが降ってきて、かえって設定が複雑になりがちなので、公式ドキュメントや信頼できる記事をしっかり読んで設定を確認するのがやはり大事と感じました。
「とりあえず動く」実装にはなったものの、まだ不要なものが含まれている可能性がありますが、何かの参考になれば幸いです。もし整理してもっとスマートな設定にした方が良い場合は、この記事を参考にしつつ適宜削ぎ落としてみてください。
以上、AWS CDK を使って Qdrant + ECS + EFS を構成するときのメモでした。