🗻

CVATでS3をファイルシステムとしてマウントする

2024/01/13に公開

背景

CVATのデータソースとしてS3を使用する際、CloudStorageで接続する方法だと以下の問題がある。

  • bucketを紐づける際、毎回manifest.jsonlを作成しないといけないのが面倒
  • 一つのCloudStorageにGUIからだと5つまでしかmanifest.jsonlを紐付けれらない(OSSなのでSelf-hostedの場合自分でリミットを外せるが、問題無いのかは未調査)
  • manifest.jsonl 5つまでの制約を回避するために、新規画像群をアップロードする際bucketを新規作成してそれをCloudStorageに紐づける運用も考えられるが、bucketが乱立し見た目が悪い

そこでCVATのdocsを読んでいたら、S3をマウントしてファイルシステムとして Connected file share から使用できる方法があったので、それを試す。

やること

  • TerraformでEC2とS3を立ち上げる
  • CVATをEC2上に立てる
  • s3fsを使ってS3をマウントしてCVATの Connected file share からアクセスできるようにする

参考

https://opencv.github.io/cvat/v2.9.2/docs/administration/advanced/mounting_cloud_storages/#aws-s3-bucket-as-filesystem
https://github.com/s3fs-fuse/s3fs-fuse

環境

  • OS: macOS Ventura 13.5
  • AWS CLI: aws-cli/2.8.3 Python/3.9.11 Darwin/22.6.0 exe/x86_64 prompt/off
  • tfenv: tfenv 3.0.0
  • terraform: Terraform v1.6.6 on darwin_amd64

1. TerraformでS3とEC2を立てる

terraform用のディレクトリを作成

コマンドを実行
mkdir terraform
コマンドを実行
cd terraform

IAMユーザーを作成

AWS Consoleをぽちぽちして作成する
(僕はtmp-for-create-ec2-and-s3という名前で作りました)

新規プロファイルを登録

コマンドを実行
aws configure --profile <profile_name>
  • <profile_name>: tmp-for-create-ec2-and-s3
値を入力
AWS Access Key ID [None]: <aws_access_key_id>
AWS Secret Access Key [None]: <aws_secret_access_key>
Default region name [None]: <region_name>
Default output format [None]: 
  • <aws_access_key_id>: 正しい値を入力
  • <aws_secret_access_key>: 正しい値を入力
  • <region_name>: ap-northeast-1

確認

コマンドを実行
cat ~/.aws/config
出力例
...
[profile tmp-for-create-ec2-and-s3]
region = ap-northeast-1
コマンドを実行
cat ~/.aws/credentials
出力例
...
[tmp-for-create-ec2-and-s3]
aws_access_key_id = *****
aws_secret_access_key = *****

versions.tfを作成

コマンドを実行
touch versions.tf
versions.tf
terraform {
  required_providers {
    aws = {
      source  = "hashicorp/aws"
      version = ">= 4.49.0"
    }
  }

  required_version = "~> 1.6.0"
}

provider "aws" {
    profile = var.profile
}

variables.tfを作成

コマンドを実行
touch variables.tf
variables.tf
variable "profile" {
    type = string
}

variable "project" {
  type = string
}

variable "environment" {
  type = string
}

variable "vpc_cidr" {
  type        = string
  description = "vpc cidrblock"
}

variable "subnet_cidr" {
  type        = string
  description = "public subnet cidr"
}

terraform.tfvarsを作成

コマンドを実行
touch terraform.tfvars
terraform.tfvars
project = <project_name>

environment = "dev"

vpc_cidr = "10.0.0.0/16"

subnet_cidr = "10.0.1.0/24"

profile = <profile_name>
  • <project_name>: "tmp-for-create-ec2-and-s3"
  • <profile_name>: "tmp-for-create-ec2-and-s3"

network.tfを作成

コマンドを実行
touch network.tf
network.tf
# VPC
resource "aws_vpc" "main" {
  cidr_block                       = var.vpc_cidr
  instance_tenancy                 = "default"
  assign_generated_ipv6_cidr_block = false

  tags = {
    Name    = "${var.project}-${var.environment}-vpc"
    Project = var.project
    Env     = var.environment
  }
}

# Subnet
resource "aws_subnet" "public" {
  vpc_id                  = aws_vpc.main.id
  availability_zone       = <availability_zone>
  cidr_block              = var.subnet_cidr
  map_public_ip_on_launch = true

  tags = {
    Name    = "${var.project}-${var.environment}-public-subnet"
    Project = var.project
    Env     = var.environment
    Type    = "public"
  }
}


# Route table
resource "aws_route_table" "rtb" {
  vpc_id = aws_vpc.main.id

  tags = {
    Name    = "${var.project}-${var.environment}-rtb"
    Project = var.project
    Env     = var.environment
  }
}

# Route table と subnet の関連付け
resource "aws_route_table_association" "public_rtb" {
  route_table_id = aws_route_table.rtb.id
  subnet_id      = aws_subnet.public.id
}

# Internet Gateway
resource "aws_internet_gateway" "igw" {
  vpc_id = aws_vpc.main.id

  tags = {
    Name    = "${var.project}-${var.environment}-igw"
    Project = var.project
    Env     = var.environment
  }
}

# Route table と IGW の関連付け
resource "aws_route" "rtb_igw_route" {
  route_table_id         = aws_route_table.rtb.id
  destination_cidr_block = "0.0.0.0/0"
  gateway_id             = aws_internet_gateway.igw.id
}

# Security Group
resource "aws_security_group" "sg" {
  name        = "${var.project}-${var.environment}-sg"
  description = "security group"
  vpc_id      = aws_vpc.main.id

  ingress {
    from_port   = 22
    to_port     = 22
    protocol    = "tcp"
    cidr_blocks = ["0.0.0.0/0"]
  }

  ingress {
    from_port   = 8080
    to_port     = 8080
    protocol    = "tcp"
    cidr_blocks = ["0.0.0.0/0"]
  }

  egress {
    from_port   = 0
    to_port     = 0
    protocol    = "-1"
    cidr_blocks = ["0.0.0.0/0"]
  }

  tags = {
    Name    = "${var.project}-${var.environment}-sg"
    Project = var.project
    Env     = var.environment
  }
}
  • <availability_zone>: "ap-northeast-1a"

data.tfを作成

コマンドを実行
touch data.tf
data.tf
data "aws_ami" "ubuntu" {
  most_recent = true

  filter {
    name   = "name"
    values = ["ubuntu/images/hvm-ssd/ubuntu-focal-20.04-amd64-server-*"]
  }

  filter {
    name   = "virtualization-type"
    values = ["hvm"]
  }

  owners = ["099720109477"] # Canonical
}

ec2.tfを作成

コマンドを実行
touch ec2.tf
ec2.tf
resource "aws_instance" "ec2" {
  ami                         = data.aws_ami.ubuntu.id
  instance_type               = "t3.large"
  subnet_id                   = aws_subnet.public.id
  associate_public_ip_address = true
  vpc_security_group_ids      = [aws_security_group.sg.id]

  root_block_device {
    volume_size = 20
  }

  tags = {
    Name    = "${var.project}-${var.environment}-ec2"
    Project = var.project
    Env     = var.environment
  }
}

s3.tfを作成

コマンドを実行
touch s3.tf
s3.tf
resource "aws_s3_bucket" "s3" {
  bucket = "${var.project}-${var.environment}-bucket"
}

terraform init

コマンドを実行
terraform init
出力
...
Terraform has been successfully initialized!
...

yeah!

terraform plan

コマンドを実行
terraform plan
出力
...
Plan: 9 to add, 0 to change, 0 to destroy.

yeah!!

terraform apply

コマンドを実行
terraform apply
出力
...
Apply complete! Resources: 9 added, 0 changed, 0 destroyed.

yeah!!!

2. bucketに画像をアップロードする

AWSのConsleでぽちぽちアップロードする

3. EC2 Instance Connectで接続してターミナルを起動




黒い画面が出てきたらok

4. CVATをインストールする

Docker と Docker Compose をインストール

「EC2上で実行」とあるコマンドは全て、先ほどEC2に接続したターミナルで実行します。

EC2上で実行
sudo apt-get update
EC2上で実行
sudo apt-get --no-install-recommends install -y \
apt-transport-https \
ca-certificates \
curl \
gnupg-agent \
software-properties-common
EC2上で実行
curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo apt-key add -
EC2上で実行
sudo add-apt-repository \
"deb [arch=amd64] https://download.docker.com/linux/ubuntu \
$(lsb_release -cs) \
stable"
EC2上で実行
sudo apt-get update
EC2上で実行
sudo apt-get --no-install-recommends install -y \
docker-ce docker-ce-cli containerd.io docker-compose-plugin

root権限無しにdockerを実行できるように設定

EC2上で実行
sudo groupadd docker
EC2上で実行
sudo usermod -aG docker $USER

ここで、設定を反映させるため、一度セッションを閉じて再度Connectする

CVAT を clone する

EC2上で実行
git clone https://github.com/opencv/cvat
EC2上で実行
cd cvat

CVAT_HOST環境変数をセットする

EC2上で実行
export CVAT_HOST=<your-ip-address>
  • <your-ip-address>: EC2 の public IP address

5. S3をマウントするための設定をする

EC2上にs3fsをインストールする

EC2上で実行
sudo apt install s3fs

認証情報を設定する

EC2上で実行
echo <ACCESS_KEY_ID>:<SECRET_ACCESS_KEY> > ${HOME}/.passwd-s3fs
  • <ACCESS_KEY_ID>: 正しい値にする
  • <SECRET_ACCESS_KEY>: 正しい値にする
EC2上で実行
chmod 600 ${HOME}/.passwd-s3fs

user_allow_otherのコメントアウトを外す

EC2上で実行
sudo vi /etc/fuse.conf
/etc/fuse.conf
...
user_allow_other # <- ここのコメントアウトを外す
...

マウント先のフォルダを作成する

EC2上で実行
mkdir images

マウントのためのコマンドを実行する

EC2上で実行
s3fs <bucket_name> <mount_point> -o allow_other -o passwd_file=${HOME}/.passwd-s3fs -o url=https://s3-<region>.amazonaws.com -o use_path_request_style
  • <bucket_name>: tmp-for-create-ec2-and-s3-dev-bucket
  • <mount_point>: /home/ubuntu/cvat/images
  • <region>: ap-northeast-1

-o url=https://s3-<region>.amazonaws.com -o use_path_request_styleが無いとエラーが出る。(参照: https://github.com/s3fs-fuse/s3fs-fuse/issues/666#issuecomment-475407515)

マウントできているか確認

EC2上で実行
cat /etc/mtab | grep 's3fs'
出力
s3fs /home/ubuntu/cvat/images fuse.s3fs rw,nosuid,nodev,relatime,user_id=1000,group_id=1000,allow_other 0 0

docker-compose.share.ymlを作成する

EC2上で実行
touch docker-compose.share.yml
EC2上で実行
vi docker-compose.share.yml
docker-compose.share.yml
services:
  cvat_server:
    volumes:
      - cvat_share:/home/django/share:ro
  cvat_worker_import:
    volumes:
      - cvat_share:/home/django/share:ro
  cvat_worker_export:
    volumes:
      - cvat_share:/home/django/share:ro
  cvat_worker_annotation:
    volumes:
      - cvat_share:/home/django/share:ro

volumes:
  cvat_share:
    driver_opts:
      type: none
      device: <path_to_mount>
      o: bind
  • <path_to_mount>: /home/ubuntu/cvat/images

6. CVATを立ち上げる

EC2上で実行
docker compose -f docker-compose.yml -f docker-compose.share.yml up -d

7. superuser を設定する

EC2上で実行
docker exec -it cvat_server bash -ic 'python3 ~/manage.py createsuperuser'

8. connected file share 内の画像で task を作ってみる

http://<your-ip-address>:8080にアクセス






成功🎉

9. お片付け

bucket(tmp-for-create-ec2-and-s3-dev-bucket)を空にする

AWS Console でぽちぽちして空にする

Terraformで作った環境を削除

ローカルで実行
terraform destroy

作ったIAM userを削除

AWS Console でぽちぽちして削除する

登録したprofileを削除

config内の記述を削除する

ローカルで実行
vi ~/.aws/config
~/.aws/config (例)
...
[profile tmp-for-create-ec2-and-s3] # <- この行を削除
region = ap-northeast-1 # <- この行を削除

credentials内の記述を削除する

ローカルで実行
vi ~/.aws/credentials
~/.aws/credentials (例)
...
[tmp-for-create-ec2-and-s3] # <- この行を削除
aws_access_key_id = ***** # <- この行を削除
aws_secret_access_key = ***** # <- この行を削除

terraformディレクトリを削除

ローカルで実行
cd ../
ローカルで実行
rm -rf terraform

Discussion