ECSからS3をマウントしてみた
こんにちは。
ご機嫌いかがでしょうか。
"No human labor is no human error" が大好きな吉井 亮です。
Mountpoint for Amazon S3 が発表されました。
Linux から直接 S3 バケットに対して読み書き機能を提供するソフトウェアです。細かく書くと、読み取りと新規ファイル作成を行うものです。
データレイクからファイルを読み取り何らかの処理をして結果ファイルをデータレイクへ戻す、みたいな処理に適切です。
処理をするアプリケーションによっては S3 API をネイティブにサポートしていないことは珍しくないはずで、その際は前後に S3 Get/Put する別処理が必要でしたが、Mountpoint for Amazon S3 によってファイルシステムとしてマウント可能になれば別処理は要らなくなります。
ただ、S3 はオブジェクトストレージであることには変わりありません。ファイルサーバーのような使い方はできない、というか向かないのでファイルサーバー用途にこの Mountpoint for Amazon S3 を使うことは控えたほうがよいと思います。
やってみた
コンテナアプリケーションでも S3 からデータを Get して処理、結果を Put というパターンは多く存在すると想像しています。
コンテナ内の作業ディレクトリに Get/Put する手間を省くことになれば幸せということで ECS から S3 をマウントしてみます。
今回は ECS on EC2 です。
ECS on Fargate でマウントはまだできないようなので、ECS on Fargate で S3 バケット上のファイルを読みたい場合は API や AWS CLI を検討ください。
Mount failed on ECS Fargate container #450
Amazon ECS-optimized AMI に Mountpoint for Amazon S3 をインストール
データプレーンとなる EC2 は Amazon ECS-optimized AMI から起動します。
ECS クラスターのキャパシティプロパイダーで指定しているオートスケーリンググループの起動テンプレートにてユーザーデータを追加します。
## 既存のユーザーデータに以下を追加
## mountpoint-3
### Graviton インスタンス はこちら
curl -o /tmp/mount-s3.rpm https://s3.amazonaws.com/mountpoint-s3-release/latest/arm64/mount-s3.rpm
### x86_64 はこちら
curl -o /tmp/mount-s3.rpm https://s3.amazonaws.com/mountpoint-s3-release/latest/x86_64/mount-s3.rpm
yum install -y /tmp/mount-s3.rpm
mount-s3 your_bucket_name /mnt
rm /tmp/mount-s3.rpm
上のユーザーデータが正常に実行されれば EC2 インスタンスの /mnt に S3 バケットがマウントされるようになります。もちろん /mnt は任意に変更して大丈夫です。
インスタンスプロファイルにポリシーを追加
データプレーンとなる EC2 のインスタンスプロファイルにポリシーを追加します。マウント先バケットに対して5つの権限が最低限必要なようです。
Configuring Mountpoint for Amazon S3
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "MountpointFullBucketAccess",
"Effect": "Allow",
"Action": [
"s3:ListBucket"
],
"Resource": [
"arn:aws:s3:::your_bucket_name"
]
},
{
"Sid": "MountpointFullObjectAccess",
"Effect": "Allow",
"Action": [
"s3:GetObject",
"s3:PutObject",
"s3:AbortMultipartUpload",
"s3:DeleteObject"
],
"Resource": [
"arn:aws:s3:::your_bucket_name/*"
]
}
]
}
ECS タスク定義でバインドマウント
ECS タスク定義に数行追加してマウントします。
containerPath はコンテナ内のマウントポイントです。
sourcePath にはユーザーデータの mount-s3 your_bucket_name /mnt
のマウント先で指定したマウントポイントを記述します。
"containerDefinitions": [
(省略)
{
"mountPoints": [
{
"sourceVolume": "s3root",
"containerPath": "/mnt"
}
}
(省略)
],
(省略)
"volumes": [
{
"name": "s3root",
"host": {
"sourcePath": "/mnt"
}
}
],
(省略)
タスク実行
ls /mnt
を打つだけのタスクを作って動作確認しました。
クリックして展開
{
"containerDefinitions": [
{
"name": "ryoyoshii_mount_s3",
"image": "public.ecr.aws/amazonlinux/amazonlinux:2023",
"cpu": 0,
"portMappings": [],
"essential": true,
"command": [
"ls",
"/mnt"
],
"environment": [],
"mountPoints": [
{
"sourceVolume": "s3root",
"containerPath": "/mnt"
}
],
"volumesFrom": [],
"logConfiguration": {
"logDriver": "awslogs",
"options": {
"awslogs-group": "/ecs/cluster_name",
"awslogs-region": "us-west-2",
"awslogs-stream-prefix": "mount_s3"
}
}
}
],
"family": "ryoyoshii_mount_s3",
"taskRoleArn": "your_Task_RoleArn",
"executionRoleArn": "your_Task_Execution_RoleArn",
"networkMode": "awsvpc",
"volumes": [
{
"name": "s3root",
"host": {
"sourcePath": "/mnt"
}
}
],
"requiresCompatibilities": [
"EC2"
],
"cpu": "512",
"memory": "1024",
}
terraform
一連の動作確認ができる tf ファイルを記述しておきます。よしなに編集してお使いください。
クリックして展開
locals {
// General
sysname = "ryoyoshii_test"
region = "us-west-2"
// Network
vpcid = "vpc-"
subnet = {
public_a = "subnet-"
public_b = "subnet-"
public_c = "subnet-"
}
work_bucket_name = "ryoyoshii-tfstate"
}
// ECS Cluster
resource "aws_ecs_cluster" "this" {
name = local.sysname
setting {
name = "containerInsights"
value = "enabled"
}
tags = {
Name = "${local.sysname}"
}
}
// End of ECS Cluster
// Task definition
//// mount-s3
resource "aws_ecs_task_definition" "mount_s3" {
family = "${local.sysname}_mount_s3"
container_definitions = jsonencode([
{
"logConfiguration" : {
"logDriver" : "awslogs",
"options" : {
"awslogs-group" : aws_cloudwatch_log_group.ecs.name,
"awslogs-region" : "${local.region}",
"awslogs-stream-prefix" : "mount_s3"
}
},
"command" : [
"ls",
"/mnt"
],
"essential" : true,
"volumesFrom" : [],
"mountPoints" : [
{
"sourceVolume" : "s3root",
"containerPath" : "/mnt"
}
],
"image" : "public.ecr.aws/amazonlinux/amazonlinux:2023",
"name" : "${local.sysname}_mount_s3"
}
])
volume {
name = "s3root"
host_path = "/mnt"
}
task_role_arn = aws_iam_role.ecs_Task.arn
execution_role_arn = aws_iam_role.ecs_TaskExecution.arn
requires_compatibilities = ["EC2"]
cpu = 512
memory = 1024
network_mode = "awsvpc"
}
// End of Task definition
// Security Group ECS
resource "aws_security_group" "ecs" {
name = "${local.sysname}_ecs_sg"
description = "ECS ${local.sysname}"
vpc_id = local.vpcid
egress {
from_port = 0
to_port = 0
protocol = "-1"
cidr_blocks = ["0.0.0.0/0"]
}
tags = {
Name = "${local.sysname}_ecs_sg"
}
}
// End Of Security Group ECS
// IAM Role TaskExecution
resource "aws_iam_role" "ecs_TaskExecution" {
name = "CustomRoleTaskExecution_${local.sysname}"
assume_role_policy = jsonencode({
"Version" : "2012-10-17",
"Statement" : [
{
"Effect" : "Allow",
"Principal" : {
"Service" : "ecs-tasks.amazonaws.com"
},
"Action" : "sts:AssumeRole"
}
]
})
}
resource "aws_iam_role_policy_attachment" "ecs_TaskExecution" {
role = aws_iam_role.ecs_TaskExecution.name
policy_arn = "arn:aws:iam::aws:policy/service-role/AmazonECSTaskExecutionRolePolicy"
}
// End Of IAM Role TaskExecution
// IAM Role ECS Task
resource "aws_iam_role" "ecs_Task" {
name = "CustomRoleTask_${local.sysname}"
assume_role_policy = jsonencode({
"Version" : "2012-10-17",
"Statement" : [
{
"Effect" : "Allow",
"Principal" : {
"Service" : "ecs-tasks.amazonaws.com"
},
"Action" : "sts:AssumeRole"
}
]
})
inline_policy {
name = "CustomRoleTask_${local.sysname}"
policy = jsonencode({
Version = "2012-10-17"
Statement = [
{
"Effect" : "Allow",
"Action" : "s3:ListAllMyBuckets",
"Resource" : "arn:aws:s3:::*"
},
{
"Effect" : "Allow",
"Action" : [
"s3:ListBucket",
"s3:ListBucketVersions"
]
"Resource" : "arn:aws:s3:::${local.work_bucket_name}"
},
{
"Effect" : "Allow",
"Action" : [
"s3:GetObject",
"s3:PutObject"
],
"Resource" : "arn:aws:s3:::${local.work_bucket_name}/*"
}
]
})
}
}
resource "aws_iam_role_policy_attachment" "ecs_Task" {
role = aws_iam_role.ecs_Task.name
policy_arn = "arn:aws:iam::aws:policy/service-role/AmazonECSTaskExecutionRolePolicy"
}
// End Of IAM Role ECS Task
// CloudWatch Logs Group
resource "aws_cloudwatch_log_group" "ecs" {
name = "/ecs/${local.sysname}"
retention_in_days = 7
}
// End Of CloudWatch Logs Group
// ECS on EC2 s3 mounted
data "aws_ssm_parameter" "ecs_ami_id" {
name = "/aws/service/ecs/optimized-ami/amazon-linux-2023/arm64/recommended/image_id"
}
// ecs capacity provider
resource "aws_ecs_cluster_capacity_providers" "this" {
cluster_name = aws_ecs_cluster.this.name
capacity_providers = [
aws_ecs_capacity_provider.this.name,
]
default_capacity_provider_strategy {
base = 0
weight = 1
capacity_provider = aws_ecs_capacity_provider.this.name
}
}
resource "aws_ecs_capacity_provider" "this" {
name = local.sysname
auto_scaling_group_provider {
auto_scaling_group_arn = aws_autoscaling_group.this.arn
managed_scaling {
maximum_scaling_step_size = 10000
minimum_scaling_step_size = 1
status = "ENABLED"
target_capacity = 100
}
}
}
resource "aws_autoscaling_group" "this" {
name = local.sysname
max_size = 1
min_size = 0
health_check_grace_period = 0
health_check_type = "EC2"
desired_capacity = 0
vpc_zone_identifier = [
local.subnet.public_a,
local.subnet.public_b,
local.subnet.public_c
]
mixed_instances_policy {
instances_distribution {
on_demand_base_capacity = 0
on_demand_percentage_above_base_capacity = 0
spot_allocation_strategy = "capacity-optimized"
}
launch_template {
launch_template_specification {
launch_template_id = aws_launch_template.this.id
}
}
}
tag {
key = "AmazonECSManaged"
value = ""
propagate_at_launch = true
}
}
resource "aws_launch_template" "this" {
name = local.sysname
image_id = data.aws_ssm_parameter.ecs_ami_id.value
iam_instance_profile {
name = aws_iam_instance_profile.this.name
}
user_data = filebase64("userdata.sh")
update_default_version = true
vpc_security_group_ids = [aws_security_group.this.id]
instance_requirements {
vcpu_count {
min = 1
max = 4
}
memory_mib {
min = 1024
max = 8096
}
instance_generations = ["current"]
}
block_device_mappings {
device_name = "/dev/xvda"
ebs {
delete_on_termination = "true"
encrypted = "true"
volume_size = 30
volume_type = "gp3"
}
}
tag_specifications {
resource_type = "instance"
tags = {
Name = local.sysname
}
}
}
// End of ecs capacity provider
// Security Group EC2
resource "aws_security_group" "this" {
name = "${local.sysname}-ec2-sg"
description = "ECS on EC2"
vpc_id = local.vpcid
egress {
from_port = 0
to_port = 0
protocol = "-1"
cidr_blocks = ["0.0.0.0/0"]
}
tags = {
Name = "${local.sysname}-ec2-sg"
}
}
// End of Security Group EC2
// Instance Profile
data "aws_iam_policy_document" "this" {
statement {
actions = ["sts:AssumeRole"]
principals {
type = "Service"
identifiers = [
"ec2.amazonaws.com"
]
}
}
}
data "aws_iam_policy_document" "inline_policy" {
statement {
actions = ["s3:ListBucket"]
effect = "Allow"
resources = ["arn:aws:s3:::${local.work_bucket_name}"]
}
statement {
actions = [
"s3:GetObject",
"s3:PutObject",
"s3:AbortMultipartUpload",
"s3:DeleteObject"
]
effect = "Allow"
resources = ["arn:aws:s3:::${local.work_bucket_name}/*"]
}
}
resource "aws_iam_role" "this" {
name = "CustomRole-${local.sysname}-ECSonEC2"
assume_role_policy = data.aws_iam_policy_document.this.json
inline_policy {
name = "my_inline_policy"
policy = data.aws_iam_policy_document.inline_policy.json
}
}
resource "aws_iam_instance_profile" "this" {
name = "CustomRole-${local.sysname}-ECSonEC2"
role = aws_iam_role.this.name
}
resource "aws_iam_role_policy_attachment" "this" {
role = aws_iam_role.this.name
policy_arn = "arn:aws:iam::aws:policy/AmazonSSMManagedInstanceCore"
}
resource "aws_iam_role_policy_attachment" "ecs_ec2_role" {
role = aws_iam_role.this.name
policy_arn = "arn:aws:iam::aws:policy/service-role/AmazonEC2ContainerServiceforEC2Role"
}
// End Of Instance Profile
Dockerfileに書くパターン
ホストでマウントせずとも Dockerfile に Mountpoint for Amazon S3 をインストールするパターンも可能です。単純な優劣はないと思うので要件に合わせて使い分けると便利だと思います。
Running Mountpoint for Amazon S3 in a Docker container
参考
ECS ではない普通の EC2 インスタンスでマウントしたい場合は AWS製のS3マウントツール「Mountpoint for Amazon S3」を試してみた を参照ください。
Discussion
ホストでS3バケットをマウントしてそこにコンテナからバインドマウントするか、コンテナの中でMountpoint for Amazon S3使って直接S3バケットとマウントするか検証時に迷ったのですが、ECS on EC2、ECS on Fargate(現状マウントできない)、macOS、WindowsでそれぞれS3バケットとマウントする方式が異なると思ったので、
という感じにすればコンテナ側であまりホストとS3バケットのマウント方式の違いを意識しなくて良くなるのでは?と考えたのですがどうでしょう?
良いアイデアだと思います。
賛成です。