内容
AWS CDK - Python Lambda Layer デプロイ で、
外部パッケージを Layer や Lambda に組み込んだが、
Lambda に 250MB のサイズ制限があるため、
pandas 等、重いパッケージを入れるとすぐに制限に引っかかる
そのため、 EFS に外部パッケージをインストールし、Lambda から使用することでこれを回避する
EFS への外部パッケージインストールは ECS を使用する
また、CDKデプロイで自動的に環境を構築したいので、
Custom Resource で ECS Task を起動する
実装
VPC
適当な VPC 環境一式と、
EFS と Lambda に割り当てる Security Group を作成し、
インバウンドルールで Lambda から EFS へのアクセスを許可している
// VPC
const vpc = new ec2.Vpc(this, `${stackName}-Vpc`, {
ipAddresses: ec2.IpAddresses.cidr('10.0.0.0/16'),
maxAzs: 2,
subnetConfiguration: [
{ name: 'Public', subnetType: ec2.SubnetType.PUBLIC, cidrMask: 24 },
{
name: 'Private',
subnetType: ec2.SubnetType.PRIVATE_WITH_EGRESS,
cidrMask: 24,
},
{
name: 'Isolated',
subnetType: ec2.SubnetType.PRIVATE_ISOLATED,
cidrMask: 24,
},
],
});
const privateSubnets = vpc.selectSubnets({
subnetType: ec2.SubnetType.PRIVATE_WITH_EGRESS,
});;
// Security Group for client
const clientSg = new ec2.SecurityGroup(this, `${stackName}-ClientSg`, {
vpc,
securityGroupName: `${stackName}-ClientSg`,
allowAllOutbound: true,
});
// Security Group for EFS
const efsSg = new ec2.SecurityGroup(this, `${stackName}-EfsSg`, {
vpc,
securityGroupName: `${stackName}-EfsSg`,
allowAllOutbound: true,
});
efsSg.addIngressRule(ec2.Peer.securityGroupId(clientSg.securityGroupId), ec2.Port.tcp(2049));
EFS
// EFS
const efsFileSystem = new efs.FileSystem(this, `${stackName}-EfsFileSystem`, {
vpc,
fileSystemName: `${stackName}-EfsFileSystem`,
securityGroup: efsSg,
vpcSubnets: privateSubnets,
});
const efsAccessPoint = new efs.AccessPoint(this, `${stackName}-EfsAccessPoint`, {
fileSystem: efsFileSystem,
createAcl: {
ownerUid: '1001',
ownerGid: '1001',
permissions: '755',
},
path: `/${stackName}`,
posixUser: {
uid: '1001',
gid: '1001',
},
});
Lambda
VPC Lambda に必要なポリシー(service-role/AWSLambdaVPCAccessExecutionRole
)、
EFS アクセスに必要なポリシー(AmazonElasticFileSystemClientReadWriteAccess
)をアタッチする
// Lambda
const efsMountPath = '/mnt/efs';
const sampleFunctionName = `${stackName}-sampleFunction`;
const sampleFunctionRole = new iam.Role(this, `${sampleFunctionName}Role`, {
roleName: `${sampleFunctionName}Role`,
assumedBy: new iam.ServicePrincipal('lambda.amazonaws.com'),
managedPolicies: [
iam.ManagedPolicy.fromAwsManagedPolicyName('service-role/AWSLambdaBasicExecutionRole'),
iam.ManagedPolicy.fromAwsManagedPolicyName('service-role/AWSLambdaVPCAccessExecutionRole'),
iam.ManagedPolicy.fromAwsManagedPolicyName('AmazonElasticFileSystemClientReadWriteAccess'),
],
});
new lambda.Function(this, sampleFunctionName, {
code: lambda.Code.fromAsset(path.join(__dirname, 'handlers/sample-func')),
handler: 'index.handler',
runtime: lambda.Runtime.PYTHON_3_11,
environment: {
/* eslint-disable @typescript-eslint/naming-convention */
EFS_MOUNT_PATH: efsMountPath,
/* eslint-enable @typescript-eslint/naming-convention */
},
filesystem: lambda.FileSystem.fromEfsAccessPoint(efsAccessPoint, efsMountPath),
functionName: sampleFunctionName,
logRetention: logs.RetentionDays.ONE_MONTH,
role: sampleFunctionRole,
securityGroups: [clientSg],
timeout: Duration.seconds(30),
tracing: lambda.Tracing.ACTIVE,
vpc,
vpcSubnets: privateSubnets,
});
Lambda 関数では、
sys.path.append
で、マウントした EFS を import パスに追加する
import os
import sys
efs_mount_path = os.getenv("EFS_MOUNT_PATH")
if type(efs_mount_path) is str:
sys.path.append(efs_mount_path)
import pandas as pd # type: ignore
def handler(event, context):
df = pd.DataFrame(data=["a1", "a2", "a3"])
print(df)
ECS
ECS に EFS をマウントし、
EFS 上にPythonパッケージのインストール(pip install -r requirements.txt -t ${efsMountPath}
)を行うタスクを定義する
// ECR
const dockerImageAsset = new ecrAssets.DockerImageAsset(this, `${stackName}-EcrDockerImage`, {
directory: `${__dirname}/docker`,
});
// ECS
const ecsTaskExecutionRole = new iam.Role(this, `${stackName}-EcsTaskExecutionRole`, {
assumedBy: new iam.ServicePrincipal('ecs-tasks.amazonaws.com'),
managedPolicies: [iam.ManagedPolicy.fromAwsManagedPolicyName('service-role/AmazonECSTaskExecutionRolePolicy')],
roleName: `${stackName}-EcsTaskExecutionRole`,
});
const ecsTaskRole = new iam.Role(this, `${stackName}-EcsTaskRole`, {
assumedBy: new iam.ServicePrincipal('ecs-tasks.amazonaws.com'),
managedPolicies: [iam.ManagedPolicy.fromAwsManagedPolicyName('AmazonElasticFileSystemClientReadWriteAccess')],
roleName: `${stackName}-EcsTaskRole`,
});
const cluster = new ecs.Cluster(this, `${stackName}-EcsCluster`, {
clusterName: `${stackName}-EcsCluster`,
containerInsights: true,
vpc,
});
const ecsCpu = 256;
const ecsMemory = 512;
const taskVolumeName = 'efs-task-volume';
const taskDefinition = new ecs.FargateTaskDefinition(this, `${stackName}-FargateTaskDefinition`, {
cpu: ecsCpu,
executionRole: ecsTaskExecutionRole,
memoryLimitMiB: ecsMemory,
taskRole: ecsTaskRole,
});
taskDefinition.addVolume({
name: taskVolumeName,
efsVolumeConfiguration: {
fileSystemId: efsFileSystem.fileSystemId,
authorizationConfig: {
accessPointId: efsAccessPoint.accessPointId,
},
transitEncryption: 'ENABLED',
},
});
const modulePackagingContainer = taskDefinition.addContainer(`${stackName}-ModulePackagingContainer`, {
image: ecs.ContainerImage.fromEcrRepository(dockerImageAsset.repository, dockerImageAsset.assetHash),
command: [`pip install -r requirements.txt -t ${efsMountPath}`],
cpu: ecsCpu,
logging: ecs.LogDrivers.awsLogs({
streamPrefix: `${stackName}-ModulePackagingContainer`,
}),
memoryLimitMiB: ecsMemory,
});
modulePackagingContainer.addMountPoints({
containerPath: efsMountPath,
readOnly: false,
sourceVolume: taskVolumeName,
});
Lambda と同環境のイメージをベースに requirements.txt
を配置する
FROM public.ecr.aws/lambda/python:3.11
RUN yum update -y
WORKDIR /app
COPY requirements.txt ./
ENTRYPOINT ["/bin/bash", "-l", "-c"]
version: '3.8'
services:
python-lambda-packages:
container_name: python-lambda-packages
build:
context: .
dockerfile: Dockerfile
pandas==2.1.1
Custom Resource
AwsCustomResource を使用する
onCreate
, onUpdate
に指定する action は、
@aws-sdk/client-ecs の Client Commands (Operations List) より選択、
parameters は、RunTaskCommand のパラメータに従って設定する
// Custom Resource
const ecsTaskRunFunctionName = `${stackName}-EcsTaskRunFunction`;
const ecsTaskRunFunctionRole = new iam.Role(this, `${ecsTaskRunFunctionName}Role`, {
assumedBy: new iam.ServicePrincipal('lambda.amazonaws.com'),
inlinePolicies: {
passrole: new iam.PolicyDocument({
statements: [
new iam.PolicyStatement({
actions: ['iam:GetRole', 'iam:PassRole'],
resources: [ecsTaskExecutionRole.roleArn, ecsTaskRole.roleArn],
}),
],
}),
ecsPolicy: new iam.PolicyDocument({
statements: [
new iam.PolicyStatement({
actions: ['ecs:RunTask'],
resources: [taskDefinition.taskDefinitionArn],
}),
],
}),
},
managedPolicies: [iam.ManagedPolicy.fromAwsManagedPolicyName('service-role/AWSLambdaVPCAccessExecutionRole')],
roleName: `${ecsTaskRunFunctionName}Role`,
});
const ecsTaskRunFunctionContent = {
action: 'RunTask',
service: 'ECS',
parameters: {
cluster: cluster.clusterArn,
launchType: 'FARGATE',
networkConfiguration: {
awsvpcConfiguration: {
subnets: privateSubnets.subnetIds,
securityGroups: [clientSg.securityGroupId],
assignPublicIp: 'DISABLED',
},
},
taskDefinition: taskDefinition.taskDefinitionArn,
},
physicalResourceId: cr.PhysicalResourceId.of(`${stackName}-EcsTaskRunCustomResource`),
};
new cr.AwsCustomResource(this, `${stackName}-EcsTaskRunCustomResource`, {
functionName: ecsTaskRunFunctionName,
logRetention: logs.RetentionDays.ONE_MONTH,
onCreate: ecsTaskRunFunctionContent,
onUpdate: ecsTaskRunFunctionContent,
role: ecsTaskRunFunctionRole,
vpc,
vpcSubnets: privateSubnets,
});
EC2 で EFS 内の確認
EC2 に ssh で接続する方法は EC2を使用したRDSに対するssh操作 にも記載しているため割愛
EFS をマウントするために amazon-efs-utils ツールをインストールする必要がある
sudo yum update
sudo yum install -y amazon-efs-utils
sudo mkdir /mnt/efs
sudo mount -t efs {ファイルシステムID}:/ /mnt/efs