同一インスタンスに複数の GPU コンテナをデプロイする【ECS on EC2】
はじめに
Amazon ECS(AWS Fargate ではなく EC2 で稼働) 上に GPU コンテナをデプロイしようとすると、「タスクが 1 インスタンスに 1 つしか立ち上がらない」問題にぶつかりました。コンテナを複数個起動してGPUリソースを共有したいのに、これじゃあECSにしてる意味ないじゃん!ってことで、EC2 上で GPU リソースを最大限に活用するための ECS コンテナ配置方法を解説したいと思います。
前提: なぜ 1 インスタンスに 1 タスクしか起動しないのか?
考えられる理由は以下の通りです。
- ECSサービスの「タスク配置」が複数のアベイラビリティーゾーンにタスクを分散する設定になっている
- 不要なGPUリソース予約を定義してしまっている
- ネットワークモード(
awsvpc
orbridge
)による制約
それではやっていきましょう!
1. ECS サービスの「タスク配置」を設定する
ECS on EC2では、タスクをどこのアベイラビリティーゾーン(AZ)に配置するかの設定があるのですが、これがデフォルトで、AZ全体にタスクを分散する設定になっているので、それを修正します。
まず、ECSサービスの「アベイラビリティゾーンの再調整」をオフにします。
「アベイラビリティゾーンの再調整」は、ECS 側の自動判定で別 AZ にタスクを移動させる機能です。これをオフにしないと、後述の「タスク配置」で「ビンパック」は指定できません。
次に、ECSサービスには「タスク配置」という項目があるので、「ビンパック」を選択します。
ビンパックはCPU やメモリをできるだけ詰め込むように配置されるため、インスタンス数を最小限に抑えることができます。
ただし、EC2インスタンスの数が多少増えるのを許容して可用性を維持したい場合は、「AZバランススプレッド」「AZバランスビンパック」の使用がおすすめです。
またカスタムで、制約を追加することができ、インスタンスのタイプやアベイラビリティゾーンを指定することもできます。
詳しくは、以下の記事を参考にしてください。
Terraformのコード
resource "aws_ecs_service" "this" {
~~~省略~~~
placement_constraints {
# インスタンスごとに複数のタスクを配置します。起動するインスタンスのタイプをg4dn.xlargeに固定します。
type = "memberOf"
expression = "attribute:ecs.instance-type == g4dn.xlarge"
}
ordered_placement_strategy {
# PUやメモリなどのリソース使用量が最小になるようにタスクを配置します。これにより、使用するインスタンス数を最小限に抑えることができます。
type = "binpack"
field = "memory"
}
~~~省略~~~
2. タスク定義で GPU リソースを指定しない
ECS タスク定義の リソース割当て制限 で GPU の設定項目で、「GPU使うから1つ設定しよ〜」と入力するかと思いますが、これは罠です。
GPU を 1 つしか持っていないインスタンス上では 1 タスクしか立てられなくなります。そのため、あえて ECS タスク定義には GPU 指定せずに空欄にして、EC2 側の Docker ランタイムに nvidia
を使う設定を組み込みます。
Terraformコード
resource "aws_ecs_task_definition" "this" {
~~~省略~~~
container_definitions = jsonencode([
{
name = "container_name"
image = "${var.ecr_repository_url}:latest"
essential = true
# GPUを指定すると、1インスタンスに1つのタスクしか配置できなくなってしまうので、コメントアウト
# resourceRequirements = [
# {
# type = "GPU"
# value = "1"
# }
# ]
~~~省略~~~
ここからの設定は、ECS on EC2に使用するEC2の起動テンプレートに関する設定です。
AMI の選択
AWS には GPU 用の最適化 AMI がいくつか用意されています。
amzn2-ami-ecs-gpu-hvm-*-x86_64-ebs
-
bottlerocket-aws-ecs-1-nvidia-aarch64-***
など
Amazon Linux や Bottlerocket などお好みに合わせて使用します。bottlerocket
が以下の記事によるといいらしいです。
参考:
Docker ランタイムの設定
EC2 起動テンプレートのユーザーデータを以下のような設定をすることで、Docker がデフォルトランタイムとして nvidia
を使うよう設定します。これにより、タスク定義で GPU リソースを指定しなくてもGPUがコンテナで使えるようになります。
EC2>起動テンプレート>高度な詳細
の一番下にユーザーデータの入力欄があります。
bash
コードをコピーする
# 例: /etc/sysconfig/docker を上書きする
sudo rm /etc/sysconfig/docker
echo DAEMON_MAXFILES=1048576 | sudo tee -a /etc/sysconfig/docker
echo OPTIONS=\"--default-ulimit nofile=32768:65536 --default-runtime nvidia\" | sudo tee -a /etc/sysconfig/docker
echo DAEMON_PIDFILE_TIMEOUT=10 | sudo tee -a /etc/sysconfig/docker
# Docker を再起動
sudo systemctl restart docker
これで、Docker コンテナ起動時に --runtime=nvidia
オプションが自動的に適用されるようになります。
参考:
4. ネットワークモードと ALB の組み合わせ
ECS on EC2では、awsvpc
かbridge
のネットワークモードを使用します。ネットワークモードはタスク定義で設定できます。ネットワークモードの違いによって、ALBやセキュリティーグループ周りの構築に違いが出てくるので解説します。
awsvpc モードとbridge モードのどちらを使用するべきか?
awsvpc モード
- 本番運用でセキュリティをより厳格にしたい
bridge モード
- ステージング環境や低コスト運用が最優先
- EC2がパブリックサブネットでも問題ない
awsvpc モードを使う場合
プライベートIPがタスクごとにアタッチされます。コンテナが外部インターネットの通信を必要としている場合は、NATGatewayの使用が必須になります。
設定例:
-
タスク定義のネットワークモードを
awsvpc
にする -
ターゲットグループのターゲットタイプを
ip
にする。もしターゲットタイプが 異なる場合はターゲットグループの再作成が必要になります。
-
NATGatewayや必要なエンドポイントを追加
外部インターネットと通信したい場合はNATGatewayは必須です。Fargateには「パブリックIPアドレスを付与する」という項目があるのですが、ECS on EC2の場合は、パブリックIPアドレスは付与されません。
注意点:
ECSインスタンス(EC2)のENI数に上限があり、同一インスタンス内に建てられるコンテナは実質14個までになります。
参考:
terraformのコード
resource "aws_ecs_task_definition" "this" {
family = "${var.project}-${var.env}-ecs-task-definition-${local.service_name}"
requires_compatibilities = ["EC2"]
network_mode = "awsvpc" //変更
cpu = 1024
memory = 4096
execution_role_arn = var.task_execution_role_arn
task_role_arn = var.task_execution_role_arn
~~~省略~~~
resource "aws_lb_target_group" "this" {
name = "${var.project}-${var.env}-tg-sbv2"
port = 80
protocol = "HTTP"
vpc_id = var.vpc_id
target_type = "ip" // 変更
~~~省略~~~
# NAT Gateway用のElastic IP
resource "aws_eip" "nat" {
domain = "vpc"
tags = {
Name = "${var.project}-eip-nat"
Project = var.project
}
}
# NAT Gateway(パブリックサブネットに配置)
resource "aws_nat_gateway" "main" {
allocation_id = aws_eip.nat.id
subnet_id = aws_subnet.public[0].id # 最初のパブリックサブネットに配置
tags = {
Name = "${var.project}-nat"
Project = var.project
}
}
~~~ルートテーブルの記述は省略~~~
# プライベートルートテーブル1 (AZ-a)にNATゲートウェイのルートを追加
resource "aws_route" "private1_nat" {
route_table_id = aws_route_table.private1.id
destination_cidr_block = "0.0.0.0/0"
nat_gateway_id = aws_nat_gateway.main.id
}
# プライベートルートテーブル2 (AZ-d)にNATゲートウェイのルートを追加
resource "aws_route" "private2_nat" {
route_table_id = aws_route_table.private2.id
destination_cidr_block = "0.0.0.0/0"
nat_gateway_id = aws_nat_gateway.main.id
}
bridge モードを使う場合
bridge
モードとALBの動的ポートと組み合わせて使用することで、ランダムなポートにコンテナを配置しつつ、ターゲットグループでそのポートを把握することができます。
設定例
- タスク定義のネットワークモードを
bridge
にする
- タスク定義の「解放するホストポート」を
0
に設定- タスク定義のポートマッピングでコンテナポートを指定し、ホストポートを 0 にすると、ECS がランダムポートを割り当てます。
- これにより、同じコンテナを複数並列で起動してもポート競合を起こしません。
-
EC2をパブリックサブネットに配置する
-
ターゲットグループのターゲットタイプを「インスタンス」にする。
もしターゲットタイプが 異なる場合はターゲットグループの再作成が必要になります。
-
EC2 側のセキュリティグループに「ALB のセキュリティグループ」からの「全てのトラフィック」を許可
Dynamic Port Mapping でランダムに割り当てられるポートに対して ALB からのアクセスを受信することができます。
terraformのコード
resource "aws_ecs_task_definition" "this" {
family = "${var.project}-${var.env}-ecs-task-definition-${local.service_name}"
requires_compatibilities = ["EC2"]
network_mode = "bridge" //変更
cpu = 1024
memory = 4096
execution_role_arn = var.task_execution_role_arn
task_role_arn = var.task_execution_role_arn
~~~省略~~~
resource "aws_security_group" "ecs" {
name = "${var.project}-${var.env}-ecs-sg-${local.service_name}"
description = "Security group for ECS instances"
vpc_id = var.vpc_id
// 全てのトラフィックを、albのセキュリティーグループに対して許可
ingress {
from_port = 0
to_port = 0
protocol = "-1"
security_groups = [var.alb_security_group_id]
}
egress {
from_port = 0
to_port = 0
protocol = "-1"
cidr_blocks = ["0.0.0.0/0"]
}
}
resource "aws_lb_target_group" "this" {
name = "${var.project}-${var.env}-tg-sbv2"
port = 80
protocol = "HTTP"
vpc_id = var.vpc_id
target_type = "instance" // 変更
~~~省略~~~
ネットワークモードのさらに詳細な解説が知りたい方は以下の記事を見てください。
結果
試しにGPUが必要なコンテナのサービスを2つデプロイしてみましょう。
ECS クラスターのコンテナインスタンスのタブで、一つのインスタンスに複数のタスクがデプロイできてたら完了です!
採用情報
架電特化のAI電話SaaSを展開しているnocall.ai株式会社では、エンジニアを積極採用中です!
ジュニア層のエンジニアからリーダー職まで幅広く募集しています。
Discussion