Launch Templates と Auto Scaling Groups で Spot Fleet Requests みたいなことをする

半年ぐらい前に、launch template と auto scaling group を組み合わせることで、オンデマンドインスタンスとスポットインスタンスを混ぜたり、インスタンスタイプを混ぜたりすることができるようになりました。
cf. Scale Amazon EC2 Instances across On-Demand, Spot and RIs in a Single Auto Scaling Group

現在 Repro では、ECS クラスタのオートスケーリングには ecs_deploy の ecs_auto_scaler という worker が spot fleet request の capacity を変更することで実現しているんですが(導入 PR)、次のような悩みがあります。

  • EC2 のコンソールを見ても、どのリクエストがどのクラスタのためのものかわからない
    • aws ec2 describe-spot-fleet-requests とかでタグを確認しないといけない
  • user data を変更するために新しいリクエストを作成すると、いちいち ecs_auto_scaler の設定の spot_fleet_request_id を変更して再起動しないといけない
  • capacity を減らした場合にどのインスタンスを terminate するか制御できない
    • auto scaling group だと特定のインスタンスを detach しつつ capacity を減らすことができる

lanunch template と auto scaling group を組み合わせることで上記の悩みを解消できる気がしたので、以下移行のための調査メモです。

Terraform によるリソースの作成

次のような感じで spot fleet request に相当する launch template と auto scaling group を作成できます。

provider "aws" {
  region = "ap-northeast-1"
}

variable "ecs_cluster" {
  default = "default"
}

variable "key_name" {}
variable "security_group_ids" {}

locals {
  user_data = <<DATA
echo ECS_CLUSTER=${var.ecs_cluster} >> /etc/ecs/ecs.config
DATA
}

resource "aws_launch_template" "ecs-test" {
  name          = "ecs-test"
  image_id      = "ami-086ca990ae37efc1b"
  instance_type = "t3-nano"

  iam_instance_profile = {
    name = "ecsInstanceRole"
  }

  key_name = "${var.key_name}"

  vpc_security_group_ids = ["${var.security_group_ids}"]

  user_data = "${base64encode(local.user_data)}"

  block_device_mappings {
    device_name = "/dev/xvda" # root device name of amazon linux2

    ebs {
      volume_type           = "gp2"
      volume_size           = 40
      delete_on_termination = true
    }
  }
}

resource "aws_autoscaling_group" "ecs-test" {
  name               = "ecs-test"
  availability_zones = ["ap-northeast-1a"]
  desired_capacity   = 2
  max_size           = 10
  min_size           = 0

  lifecycle {
    ignore_changes = ["desired_capacity"]
  }

  tag {
    key                 = "Name"
    value               = "ecs-test"
    propagate_at_launch = true
  }

  mixed_instances_policy {
    launch_template {
      launch_template_specification {
        launch_template_id = "${aws_launch_template.ecs-test.id}"
        version            = "$$Latest"

        # 次のような書き方も可
        # version = "${aws_launch_template.ecs.latest_version}"
      }

      override {
        instance_type = "c5.large"
      }

      override {
        instance_type = "m5.large"
      }

      override {
        instance_type = "r5.large"
      }
    }

    instances_distribution {
      on_demand_percentage_above_base_capacity = 0
      spot_allocation_strategy                 = "lowest-price"
      spot_instance_pools                      = "2"
    }
  }
}

launch template では launch configuration と違ってルートボリュームに関する設定ができないので、指定する AMI のルートボリューム名をハードコーディングする必要があるみたいです。
cf. amazon ec2 - Terraform AWS: override root device size using aws_launch_template and block_device_mappings - Stack Overflow

また、auto scaling group では mixed_instances_policy を指定することで複数のインスタンスタイプを扱ったり、各アベイラビリティゾーンで何個のインスタンスタイプを利用するか指定したりしています。

Launch Template を更新した時の挙動

例えば、先程の設定ファイルに次のような変更をして terraform apply してみます。

--- main.tf.orig        2019-04-01 04:48:05.000000000 +0900
+++ main.tf     2019-04-01 04:48:21.000000000 +0900
@@ -12,6 +12,7 @@
 locals {
   user_data = <<DATA
 echo ECS_CLUSTER=${var.ecs_cluster} >> /etc/ecs/ecs.config
+echo hello >> /tmp/launch.log
 DATA
 }

結果は次のとおり、launch template の変更だけです。

% terraform apply
aws_launch_template.ecs-test: Refreshing state... (ID: lt-00705b23307f215c4)
aws_autoscaling_group.ecs-test: Refreshing state... (ID: ecs-test)

An execution plan has been generated and is shown below.
Resource actions are indicated with the following symbols:
  ~ update in-place

Terraform will perform the following actions:

  ~ aws_launch_template.ecs-test
      latest_version: "3" => "0"
      user_data:      "ZWNobyBFQ1NfQ0xVU1RFUj1kZWZhdWx0ID4+IC9ldGMvZWNzL2Vjcy5jb25maWcK" => "ZWNobyBFQ1NfQ0xVU1RFUj1kZWZhdWx0ID4+IC9ldGMvZWNzL2Vjcy5jb25maWcKZWNobyBoZWxsbyA+PiAvdG1wL2xhdW5jaC5sb2cK"


Plan: 0 to add, 1 to change, 0 to destroy.

Do you want to perform these actions?
  Terraform will perform the actions described above.
  Only 'yes' will be accepted to approve.

  Enter a value: yes

aws_launch_template.ecs-test: Modifying... (ID: lt-00705b23307f215c4)
  latest_version: "3" => "<computed>"
  user_data:      "ZWNobyBFQ1NfQ0xVU1RFUj1kZWZhdWx0ID4+IC9ldGMvZWNzL2Vjcy5jb25maWcK" => "ZWNobyBFQ1NfQ0xVU1RFUj1kZWZhdWx0ID4+IC9ldGMvZWNzL2Vjcy5jb25maWcKZWNobyBoZWxsbyA+PiAvdG1wL2xhdW5jaC5sb2cK"
aws_launch_template.ecs-test: Modifications complete after 6s (ID: lt-00705b23307f215c4)

Apply complete! Resources: 0 added, 1 changed, 0 destroyed.

aws_autoscaling_group では version$Latest を指定しているので、新しく起動するインスタンスは新しいバージョンの launch template を使うことになります。各インスタンスがどのバージョンで動いているかはタグの aws:ec2launchtemplate:version で確認できます。

% aws ec2 describe-instances \
  --filters "Name=tag:Name,Values=ecs-test" \
            "Name=instance-state-name,Values=running" | \
  jq '.Reservations[].Instances[].Tags | from_entries'
{
  "aws:ec2launchtemplate:id": "lt-00705b23307f215c4",
  "Name": "ecs-test",
  "aws:ec2:fleet-id": "fleet-7a82eae8-f624-a304-0630-23880ad332cf",
  "aws:autoscaling:groupName": "ecs-test",
  "aws:ec2launchtemplate:version": "4"
}
{
  "aws:autoscaling:groupName": "ecs-test",
  "aws:ec2:fleet-id": "fleet-f2206062-7e26-290c-0e3a-89a26cef8a4f",
  "aws:ec2launchtemplate:version": "3",
  "aws:ec2launchtemplate:id": "lt-00705b23307f215c4",
  "Name": "ecs-test"
}

余談ですが、launch configuration を使って auto scaling group を定義した場合、auto scaling group の launch configuration を更新するには次のような手順を踏む必要がありました。

  1. 新しい launch configuration を作成
  2. auto scaling group に紐づく launch configuration を変更
  3. 古い launch configuration を削除(オプショナル)

Terraform では aws_launch_configurationlifecycle.create_before_destroy = true を指定すれば上記の手順で auto scaling group を更新してくれるはずなんですが、aws_launch_configuration を module 化すると新しい launch configuration の作成と古い launch configuration の削除が同時走って terraform apply に失敗するという問題があります。
launch template を使う場合、launch template の作り直しではなく新しいバージョンの作成になるため、この問題は起きなくなります。

Launch Templates & Auto Scaling Groups のデメリット

運用していくと色々見つかるかもしれませんが、デメリットとして次のことが挙げられそうです。

  • spot fleet request の capacity の単位を vCPU にしている場合、移行する際に vCPU の同じインスタンスタイプに揃える必要がある
  • Spot Diversity(Terraform の spot_instance_pools)の最小値が 2 なので、とにかくコストを抑えたい場合は高くなる