🦖

ECS on EC2(GPU)をTerraformで構築する

2022/08/16に公開約9,900字1件のコメント

こんにちは。Opt FitエンジニアのKAZYです。

弊社ではサービス構築にAWSを使用しています。
今回EC2インスタンスで運用しているサービスを一部ECSに移行しました。
その際インフラをTerraformを用いて構築したためご紹介したいと思います。

背景

一部サービスではGPUをもつEC2インスタンスで運用しています。
サービス拡大に伴いインスタンスが増えてデプロイ作業や管理が大変になってきました。
そこでECSに移行することにしました。

目標

運用コスト削減

  • これまではEC2に直接デプロイしていたのでインスタンスごとに微妙に環境や設定が異なっていましたが、コンテナアプリケーション化を可能な限りインスタンス間の差を減らします。

可用性向上

  • デプロイ時はインスタンス一つ一つに作業を行っていたため、多少のダウンタイムや作業者間で手順が異なると言ったことが起きていましたが、ECSのローリングアップデート機能を用いて最低限のダウンタイムでの更新をさせるようにします。

スケーラビリティ向上

  • 1サービス1インスタンスからロードバランサーとECSのタスク数の調節で任意のインスタンス数で動くようにします。
    • メトリクスからのタスク数のオートスケールは今回は行いません。

設計

要件を満たすためのインフラは以下のような構成になりました。
ECS FargateはGPU非対応のためECSのバックエンドをEC2とする構成になります。

ポイント

  • キャパシティプロバイダ[1]にオートスケーリンググループを付けている。
    • ローリングアップデート時に自動でインスタンスを作ったり消したりするためです。
  • オートスケーリンググループに与える起動テンプレートがGPUマシンになっている。
    • 運用するサービスはGPUを使います。
  • 複数タスクをもつサービスに負荷分散をしたいのでロードバランサーを付けている

以下が構成図です。
オートスケーリングができるキャパシティプロバイダがサービスに紐づくことでタスク実行に必要なEC2が自動で起動したり停止したりします。

以下で3つのパートに分けて説明してきます。

実行環境

❯❯❯❯ terraform -v
Terraform v1.1.8
on darwin_amd64
+ provider registry.terraform.io/hashicorp/aws v4.25.0

マシンはM1のMacです。

詳細

ECS周り(クラスター/タスク定義/サービス)

ECSの核となる部分です。

細かいことを省けばクラスター/タスク定義/サービスがあればひとまずECSのサービスとして動くものになります[2]

ロードバランサーを使ってリクエストをタスクごとに分散させたいのでサービスにロードバランサーのターゲットグループを紐付けています。

# ECSクラスター
resource "aws_ecs_cluster" "this" {
  name = "クラスター名"
}

resource "aws_ecs_service" "this" {
  name            = "サービス名"
  cluster         = aws_ecs_cluster.this.name
  task_definition = aws_ecs_task_definition.this.arn
  desired_count   = "希望するタスク数"

  # サービスが失敗続きだったら一個前にデプロイ成功したリビジョンにロールバックする機能
  deployment_circuit_breaker { 
    enable   = true
    rollback = true
  }

  deployment_maximum_percent         = 200
  deployment_minimum_healthy_percent = 100
  health_check_grace_period_seconds  = 30
  load_balancer {
    target_group_arn = "ロードバランサーのターゲットグループのARN"
    container_name   = "コンテナ名"
    # コンテナが8000番ポートを受け付けるサービス
    container_port   = 8000 
  }

  placement_constraints {
    # インスタンスごとに 1 つのタスクのみを配置します
    type = "distinctInstance" 
  }

  lifecycle {
    ignore_changes = [
      task_definition,
      desired_count,
      capacity_provider_strategy,
      # ブルーグリーン入れる場合はこちらを有効にする(ターゲットグループが切り替わるため)
      #   load_balancer, 
    ]
  }
}

resource "aws_ecs_task_definition" "this" {
  family                   = "タスク定義の名前"
  requires_compatibilities = ["EC2"]
  network_mode             = "bridge"
  cpu                      = 4096
  memory                   = 15742
  execution_role_arn       = "arn:aws:iam::${var.aws_account_id}:role/ecsTaskExecutionRole"
  task_role_arn            = "arn:aws:iam::${var.aws_account_id}:role/ecsTaskExecutionRole"

  container_definitions = jsonencode([
    {
      # クラウドウォッチログ向けの設定
      "logConfiguration" : {
        "logDriver" : "awslogs",
        "options" : {
          "awslogs-group" : "ロググループ名",
          "awslogs-region" : "リージョン",
          "awslogs-stream-prefix" : "ログストリーム名に付けたいプレフィックス"
        }
      },
      # コンテナの8000番ポートをEC2の80番ポートにマッピング
      portMappings = [
        {
          hostPort      = 80,
          protocol      = "tcp",
          containerPort = 8000,
        }
      ],
      
      linuxParameters = {
        capabilities = {
          add = [
            "SYS_ADMIN"
          ]
        },
      },
      cpu = 4096,
      # GPUを使うための設定
      environment = [
        {
          name  = "NVIDIA_DRIVER_CAPABILITIES",
          value = "all"
        }
      ],
      resourceRequirements = [
        {
          type  = "GPU",
          value = "1"
        }
      ],

      secrets = [],
      memory    = 15360,
      image     = "イメージARN",
      essential = true,
      name      = "コンテナ名"
    },
  ])
}

ロードバランサー周り(ALB/リスナールール/ターゲットグループ)

サービスへのリクエストを分散するためのロードバランサー周りの設定です。
ECSのサービスをターゲットグループにひっつけて紐づくので、こちらにECSに関する設定はありません。

resource "aws_lb" "this" {
  name               = "ロードバランサー名"
  internal           = true # 内部向け
  load_balancer_type = "application"
  security_groups    = var.security_group_ids
  subnets            = var.subnet_ids

  enable_deletion_protection = false
}

resource "aws_lb_listener" "this" {
  load_balancer_arn = aws_lb.this.arn
  port              = 80
  protocol          = "HTTP"
  default_action {
    type             = "forward"
    target_group_arn = aws_lb_target_group.this.arn
  }
}

resource "aws_lb_target_group" "this" {
  name                          = "ターゲットグループ名"
  port                          = 80
  protocol                      = "HTTP"
  protocol_version              = "HTTP1"
  vpc_id                        = var.vpc_id
  deregistration_delay          = 60
  load_balancing_algorithm_type = "round_robin"

  health_check {
    enabled             = true
    healthy_threshold   = 5
    interval            = 5
    # 正常なときのステータスコード
    matcher             = "200" 
    # 任意のヘルスチェックエンドポイント
    path                = "/health" 
    port                = "traffic-port"
    protocol            = "HTTP"
    timeout             = 2
    unhealthy_threshold = 2
  }
}

背後のEC2周り(キャパシティプロバイダー/オートスケーリング/起動テンプレート)

ECSの背後で動いているEC2に関する設定です。
キャパシティプロバイダーにオートスケーリンググループをひっつけてタスク数の要求に応じて背後で動いているEC2を増減させられるようにしています。例えば新しいバージョンをデプロイする際にサービスに支障が出ないようにタスク数を維持したままローリングアップデートさせるためには新たにEC2を起動して新しいバージョンを起動して古いバージョンのEC2を止めるという流れが必要になります。その際にEC2を増減させる役割をキャパシティプロバイダーはオートスケーリンググループと協力して行います。起動テンプレートというのは増やすEC2に関するテンプレートのことです。オートスケーリンググループにAmazonECSManagedと言うタグを付与してあげないとECSはクラスターをスケールするときにタグを管理できないので注意です。更に理解を深めたい方は以下を御覧ください。

https://docs.aws.amazon.com/ja_jp/AmazonECS/latest/developerguide/cluster-capacity-providers.html
https://docs.aws.amazon.com/ja_jp/AmazonECS/latest/developerguide/asg-capacity-providers.html
https://docs.aws.amazon.com/ja_jp/AmazonECS/latest/developerguide/cluster-auto-scaling.html
resource "aws_ecs_cluster_capacity_providers" "this" {
  cluster_name = "ECSクラスター名"

  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 = "キャパシティプロバイダー名"

  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                      = "オートスケーリンググループ名"
  max_size                  = var.max_size
  min_size                  = var.min_size
  health_check_grace_period = 0
  health_check_type         = "EC2"
  desired_capacity          = 1
  vpc_zone_identifier       = var.subnet_ids

  launch_template {
    id      = aws_launch_template.this.id
    version = "$Latest"
  }

  tag {
  # ECSにスケーリングをお願いするために必要なタグ
  # https://docs.aws.amazon.com/ja_jp/AmazonECS/latest/developerguide/cluster-auto-scaling.html#update-ecs-resources-cas
    key                 = "AmazonECSManaged" 
    value               = ""
    propagate_at_launch = true
  }


  lifecycle {
    ignore_changes = [
       # キャパシティプロバイダが必要に応じて変更するため
      desired_capacity, 
    ]
  }

}

resource "aws_launch_template" "this" {
  name = "起動テンプレート名"

  iam_instance_profile {
    arn = var.iam_instance_profile_arn
  }

  image_id = "好きなami"
  
  # GPUインスタンス
  instance_type = "g4dn.xlarge"

  key_name = "認証用の鍵名"

  vpc_security_group_ids = var.vpc_security_group_ids

  # ECS起動時に実行するスクリプト
  user_data = base64encode(templatefile("<ユーザデータのパス>/user_data.sh", { cluster_name = var.cluster_name }))
}

user_data.sh
#!/bin/bash 
# EC2をECSのクラスターに紐付けるために必要な設定
echo ECS_CLUSTER=${cluster_name} >> /etc/ecs/ecs.config;

さいごに

初めてのTerraformでECS構築は覚えることが多くて大変でした。
全体像を理解するまで結構試行錯誤しましたが運用コストと特にデプロイコストが大幅に下がったので作ってよかったなと思っています。
今後はブルーグリーンデプロイやタスクのオートスケーリングも検討していけたらと思います。

参考

今回の構成と共通する部分が多く非常に参考になりました。

https://tk-ch.hatenablog.com/entry/20220409/1649441802

🔔採用情報

ジム施設向けDXソリューションGYMDXではエンジニアを積極採用中です。
ジュニア層のエンジニアからリーダー職まで幅広く募集しています。

2022年7月にプレシリーズAラウンドにて資金調達を実施しました。

https://prtimes.jp/main/html/rd/p/000000015.000055404.html

リードエンジニア

https://herp.careers/v1/optfit/GpvSeC-fA995

バックエンドエンジニア

https://herp.careers/v1/optfit/KsDmlZ1VhTmU
脚注
  1. クラスター内のタスクで使用するインフラストラクチャをいい感じに管理するものです。 ↩︎

  2. Dockerイメージは必要ですね ↩︎

Discussion

とても参考になりました!ありがとうございます。
varで参照しているその他のリソースのコードも見れるとより嬉しいかなと思いました!

ログインするとコメントできます