FargateSpotを活用したTerraform構成 ~バランス型編~ 

# 背景と記事概要
コストカット施策の一環としてFargateSpotへの切り替えを検討・対応しています。
FargateSpotのデメリットとしていつ落ちるかわからないという問題点を抱えているため、安全に運用するためには工夫が必要です。
どうすれば良いかなと考えてた時に下記の記事を見つけました。


https://qiita.com/sugimount-a/items/6df950c0abc02c5bcb6c

非常に良い記事だったので↑を参考にしつつ実際にIaC化してみました。
個人的に難しかったところも解説入れつつ紹介しようかと思います。


# 成果物イメージ
参考記事元のバランス型を参考に以下のような構成のものを作成します。
ECS Service,Cloudwatch Alarm,AutoScalingを活用し、監視メトリクスに応じてFargate or FargateSpotをスケールアウト・インしていきます。
お手軽に試したかったので今回の監視項目対象としてはCPU使用率を利用しています。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/562264/3ac31780-7891-2e30-2907-83b3bbd58d7b.png)


# ひとまずソース
何はともあれ全体のソース。

:::note warn
VPCやらSecurityGroup,ECS Clusterやらまで書くと長くなるので割愛してます。
記載していないリソースが出てきたら裏でそれを記載しているものだと置き換えてください。
:::

```terraform
locals {
  default-name = "test"
}

# ECS
resource "aws_ecs_service" "balance" {
  name             = "${local.default-name}-balance"
  cluster          = aws_ecs_cluster.fargate-spot.id
  task_definition  = aws_ecs_task_definition.task.arn
  desired_count    = 1
  platform_version = "1.4.0"

  network_configuration {
    # 簡易的な動作確認のためpublic配置してます
    assign_public_ip = true 
    subnets          = [aws_subnet.public-1a.id,aws_subnet.public-1c.id]
    security_groups  = [aws_security_group.ecs.id]
  }
  capacity_provider_strategy {
    capacity_provider = "FARGATE_SPOT"
    base = 0
    weight = 1
  }
  capacity_provider_strategy {
    capacity_provider = "FARGATE"
    base = 0
    weight = 1
  }
}


#
# AutoScaling設定
# (決まった台数動いていればいいだけなら設定は不要)
#
resource "aws_appautoscaling_target" "balance" {
  service_namespace  = "ecs"
  resource_id        = "service/${aws_ecs_cluster.fargate-spot.name}/${aws_ecs_service.balance.name}"
  scalable_dimension = "ecs:service:DesiredCount"
  min_capacity       = 1 # 最小起動数
  max_capacity       = 4 # 最大起動数
}
resource "aws_appautoscaling_policy" "balance-scale-out" {
  name               = "${local.default-name}-balance-scale-out"
  policy_type        = "StepScaling"
  service_namespace  = aws_appautoscaling_target.balance.service_namespace
  resource_id        = aws_appautoscaling_target.balance.resource_id
  scalable_dimension = aws_appautoscaling_target.balance.scalable_dimension

  step_scaling_policy_configuration {
    adjustment_type         = "ChangeInCapacity"
    cooldown                = 30
    metric_aggregation_type = "Average"

    step_adjustment {
      # scale-outのalarm状態ならスケールアウト
      metric_interval_lower_bound = 0
      scaling_adjustment          = 1
    }
  }
}
resource "aws_appautoscaling_policy" "balance-scale-in" {
  name               = "${local.default-name}-balance-scale-in"
  policy_type        = "StepScaling"
  service_namespace  = aws_appautoscaling_target.balance.service_namespace
  resource_id        = aws_appautoscaling_target.balance.resource_id
  scalable_dimension = aws_appautoscaling_target.balance.scalable_dimension

  step_scaling_policy_configuration {
    adjustment_type         = "ChangeInCapacity"
    cooldown                = 30
    metric_aggregation_type = "Average"

    step_adjustment {
      # scale-inのalarm状態ならスケールイン
      metric_interval_upper_bound = 0
      scaling_adjustment          = -1
    }
  }
}

# scale-out,scale-inを発生させるためのメトリクス設定
resource "aws_cloudwatch_metric_alarm" "fargate_cpu_high" {
  alarm_name          = "cpu_utilization_high"
  comparison_operator = "GreaterThanOrEqualToThreshold"
  evaluation_periods  = "2"
  metric_name         = "CPUUtilization"
  namespace           = "AWS/ECS"
  period              = "60"
  statistic           = "Average"
  threshold           = "0.8"

  dimensions = {
    ClusterName = aws_ecs_cluster.fargate-spot.name
    ServiceName = aws_ecs_service.balance.name
  }

  alarm_actions = [
    aws_appautoscaling_policy.balance-scale-out.arn
  ]
}
resource "aws_cloudwatch_metric_alarm" "fargate_cpu_low" {
  alarm_name          = "cpu_utilization_low"
  comparison_operator = "LessThanOrEqualToThreshold"
  evaluation_periods  = "2"
  metric_name         = "CPUUtilization"
  namespace           = "AWS/ECS"
  period              = "60"
  statistic           = "Average"
  threshold           = "0.8"

  dimensions = {
    ClusterName = aws_ecs_cluster.fargate-spot.name
    ServiceName = aws_ecs_service.balance.name
  }

  alarm_actions = [
    aws_appautoscaling_policy.balance-scale-in.arn
  ]
}
```

# 解説
## FargateSpot設定
まず、バランス型の根幹となるECS Serviceを作成しているところです。
ここでFargateSpotを使うために必要なキャパシティープロバイダーを設定します。

```terraform
resource "aws_ecs_service" "balance" {
  # (途中割愛)

  capacity_provider_strategy {
    capacity_provider = "FARGATE_SPOT"
    base = 0
    weight = 1
  }
  capacity_provider_strategy {
    capacity_provider = "FARGATE"
    base = 0
    weight = 1
  }
}
```

ECS Clusterに紐づけられるデフォルトプロバイダーもあるのですが、
クラスターに他のサービスが紐づいている時のことを考えるとサービス単位で割り当てておいた方が無難な気がしてこのような構成にしてます。
ここは運用次第かと。

また、動作確認をしていて分かったのですが
**baseが設定されている方が優先してコンテナを建てられる**ようです。
例えばweight値を1で固定設定した場合、base値の設定を変えることで以下のようにコンテナが建てられていきます。
(とはいえ安定運用を考えるとFARGATE_SPOT:1のパターンはあんまし使わないかなぁ)

|base値|1台目 |2台目 |3台目 |
|:-|:-|:-|:-|
|FARGATE_SPOT:0<br/>FARGATE:1|FARGATE|FARGATE|FARGATE_SPOT|
|FARGATE_SPOT:1<br/>FARGATE:0|FARGATE_SPOT| FARGATE_SPOT|FARGATE|


値の設定によってコンテナの割合がどう増えていくかを解説してくれている記事もありますので、そちらも合わせて見ていただくと理解が深まるかと思います。

https://dev.classmethod.jp/articles/fargate-spot-task-count/

## オートスケーリング設定
次にオートスケーリング設定を用意します。

```terraform
resource "aws_appautoscaling_policy" "balance-scale-out" {
  #(割愛)

  step_scaling_policy_configuration {
    adjustment_type         = "ChangeInCapacity"
    cooldown                = 30
    metric_aggregation_type = "Average"

    step_adjustment {
      # scale-outのalarm状態ならスケールアウト
      metric_interval_lower_bound = 0
      scaling_adjustment          = 1
    }
  }
}
resource "aws_appautoscaling_policy" "balance-scale-in" {
  # (割愛)

  step_scaling_policy_configuration {
    adjustment_type         = "ChangeInCapacity"
    cooldown                = 30
    metric_aggregation_type = "Average"

    step_adjustment {
      # scale-inのalarm状態ならスケールイン
      metric_interval_upper_bound = 0
      scaling_adjustment          = -1
    }
  }
}
```

`metric_interval_lower_bound`,`metric_interval_upper_bound`がスケールアウト・インする値の目安となります。
lower <= 監視対象 <= upperに当てはまる場合に該当のスケーリングが行われます。
(設定値として書かれていない時は無限大(or マイナスの無限大)まで見ます)

今回はCPU使用率の閾値を80%(0.8)として設定していますが、「現在のCPU使用率 - 閾値の値」を見て実際に適用されるインスタンスの個数が変わります。

例)
<table>
    <caption>現在のCPU使用率85%の場合</caption>
    <tr>
        <td>スケールアウト設定</td>
        <td>0 < 0.05(0.85-0.8) < ∞ が当てはまる → スケールアウトされる → 1台増加</td>
    </tr>
    <tr>
        <td>スケールイン設定</td>
        <td> -∞ < 0.05(0.85-0.8) < 0 が当てはまらない → スケールインされない</td>
    </tr>
</table>

簡単な表で表すと以下のような形になります。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/562264/dd79618c-2a0b-022f-3d32-e2d9481bdc7d.png)


詳細については公式のステップ調整値をご確認ください。

https://docs.aws.amazon.com/ja_jp/autoscaling/application/userguide/application-auto-scaling-step-scaling-policies.html


# CloudWatch アラームの作成
最後にアラーム設定を作成し作成したポリシーと紐づけてやります。

```terraform
# (説明に必要なとこのみ記載してます)
resource "aws_cloudwatch_metric_alarm" "fargate_cpu_high" {
  comparison_operator = "GreaterThanOrEqualToThreshold"
  metric_name         = "CPUUtilization"
  statistic           = "Average"
  threshold           = "0.8"

  alarm_actions = [
    aws_appautoscaling_policy.balance-scale-out.arn
  ]
}
resource "aws_cloudwatch_metric_alarm" "fargate_cpu_low" {
  comparison_operator = "LessThanOrEqualToThreshold"
  metric_name         = "CPUUtilization"
  statistic           = "Average"
  threshold           = "0.8"

  alarm_actions = [
    aws_appautoscaling_policy.balance-scale-in.arn
  ]
}
```

CPU使用率を80%で監視したいので
`metric_name`に`CPUUtilization`,
`threshold`に`0.8`を指定して監視できるようにしてます。
最後にalarm_actionsにオートスケーリングポリシーを設定すれば完成となります。

# まとめ
オートスケーリングのステップ調整値が一癖あって理解するのに時間を要しましたが、
分かってしまえば簡単な形で実装できるんだなぁということがわかりました。
色々なサービスでこのバランス型は非常に使いやすいかと思いますのでぜひ挑戦してみてください。

# その他

ガンガン型はこちらで紹介してます。

https://qiita.com/satotaku113/items/3d4d1805113750579144

# 参考文献

https://qiita.com/sugimount-a/items/6df950c0abc02c5bcb6c

https://dev.classmethod.jp/articles/fargate-spot-task-count/

https://docs.aws.amazon.com/ja_jp/autoscaling/application/userguide/application-auto-scaling-step-scaling-policies.html