kubell Creator's Note

株式会社kubellのエンジニアのブログです。

ビジネスチャット「Chatwork」のエンジニアのブログです。

読者になる

スポットインスタンスの可用性をSpot Placement Scoreで事前評価する

1. はじめに

SREグループの山下(@task2021)です。

ARM スポットインスタンスは本当に十分に確保できるのか?

AWSにはSpot Placement Scoreという、「特定の条件においてスポットインスタンスをどれくらい確保しやすいか」を事前に評価できる仕組みがあります。

スポットインスタンスは、オンデマンドインスタンスと比較して大幅なコスト削減が期待できる一方で、中断される可能性があるという特性を持っています。

そのため、ワークロードの要件に適合する場合には、コスト効率の高い選択肢として非常に有効です。

「Chatwork」のEKSクラスターでは、一部のアプリケーションサーバーを除き、基本的にスポットインスタンス上でアプリケーションを動作させています。

これまで私はSpot Placement Scoreという機能を全く知らなかったのですが、「EKSノードをGraviton(ARM)に統一する」検討を進める中で、スポットインスタンスの可用性を事前に判断する材料として非常に参考になったため、本記事で紹介します。

背景:Chatwork における Graviton 統一の流れ

「Chatwork」では以前から、

  • コストダウン
  • パフォーマンス向上

の観点で、EKSノードをGravitonベースへ段階的に移行してきました。 NodeのスケーリングにはCluster Autoscalerを利用しており、Gravitonインスタンスを優先的にスケールさせる事で、結果としてほとんどのワークロードはGraviton上で動いている状態でした。

そのような状況の中で、次の一歩としてAMDノードを徐々に廃止し、Gravitonに明示的に統一するという判断を行いました。

なぜGravitonに統一したかったのか

最大の理由は、アプリケーションのDockerイメージをマルチアーキテクチャでビルドする必要がなくなることです。

段階的にGravitonベースへ移行していく中で、アプリケーションのDockerイメージのビルドは以下のような状況でした。

  • どちらのノードに配置されても問題がないよう、マルチアーキテクチャビルドを採用していた
  • 特に、buildxを使ったQEMUによるクロスビルドは設定がシンプルで、ワークフローの複雑性を抑えられるため採用しやすかった
  • 一方で、実際に運用してみるとビルド時間が長くなるという課題が顕在化した

「Chatwork」では、1日あたり3回以上デプロイが行われています。そのため、アプリケーションイメージのビルド時間は非常に大きな関心ごとです。

そこで、以下の方針を取りました。

  • アプリケーションPodをaffinityによって明示的にARMノードへ配置する
    • 仮にEKSクラスターがGravitonノードのみで構成されていたとしても、「このアプリケーションはARMでのみ動作する」という前提を暗黙知にしないため
  • QEMUを使ったマルチアーキテクチャビルドをやめ、ARMランナーでのシングルアーキテクチャビルドに切り替える
    • 別々のランナーでビルドしてマニフェストを統合する方法もありますが、そもそも今の状況であればマルチアーキテクチャビルド自体が不要

この対応により、ビルド時間を約5分短縮することができました。

新たに浮上した懸念

一方で、Gravitonに統一することで新たな問いが生まれました。

「Gravitonスポットインスタンスは、本当に安定して確保できるのか?」

「いやぁ、、大丈夫ちゃうかなぁ」という気持ちもありましたが、「絶対に枯渇しない!」とは言いきれません。

そこで使ったのがSpot Placement Score

この懸念を払拭し、自信を持ってGraviton一本化を進めていく上で、事前に評価するために使ったのがSpot Placement Scoreでした。

本記事では、

  • Spot Placement Scoreとは何か
  • Graviton(ARM)× Spotという条件で、どのように評価したのか
  • 実際に確認できた示唆

について紹介します。


2. Spot Placement Scoreとは

Spot Placement Scoreは、「指定した条件で、スポットインスタンスをどれくらい確保しやすいか」をリージョンやAZごとにスコアとして可視化する機能です。

ざっくり言うと、「この条件でスポットを取りに行ったとき、どこが通りやすそうか」を事前に把握するための指標になります。

スコアは1〜10の数値で表され、10に近いほどその条件でスポットインスタンスを確保できる可能性が高いことを示します。ただし、AWS公式ドキュメントでも明言されているとおり、あくまで推奨値であり、確保を保証するものではありません

また、このスコアはスポットインスタンスの需給状況に応じて変動するため、取得した時点のスナップショットとして捉える必要があります。

公式ドキュメントは以下にまとまっています。

docs.aws.amazon.com

EKSノードグループ設計での使い方

今回の文脈では、Spot Placement ScoreをEKSクラスターのノードグループ設計が妥当かどうかを事前に確認するために利用しました。

EKSのノードグループでは、

  • 使用するインスタンスタイプ
  • 起動するAZ
  • スポットインスタンスの割り当て方式

といった条件をあらかじめ定義します。

Spot Placement Scoreでは、これらの条件をそのまま指定してスコアを取得できるため、「このノードグループ構成で、本当にスポットインスタンスが確保できそうか」を事前に確認できるのではないかと考えました。

特に今回は、

  • ノードをGraviton(ARM)に統一する
  • スポットインスタンスを前提とした運用を継続する

という判断を行うにあたり、Gravitonインスタンスのみを指定した場合でも十分なスコアが得られるかを確認することが重要でした。

以降では、実際にどのような条件を指定してSpot Placement Scoreを確認したのかを紹介します。


3. 評価方法

現状のノードグループ構成

評価時点での「Chatwork」のEKSクラスターは、 主に以下のスポットインスタンスのノードグループで構成されていました。

  • ARMスポットインスタンス(2xlarge)× 3AZ(1b, 1c, 1d)
  • AMDスポットインスタンス(2xlarge)× 3AZ(1b, 1c, 1d)

Cluster Autoscalerでは、ARMのスポットインスタンスを最優先とし、不足した場合のみAMDのスポットインスタンスが起動する構成です。

オンデマンドインスタンスも一部併用していますが、本記事の主題ではないため詳細は割愛します。

評価の前提条件

評価は以下の条件を前提として行いました。

  • ターゲットキャパシティ: 150台
  • リージョン: ap-northeast-1
  • 対象AZ: 1b / 1c / 1d

評価パターン

Spot Placement Scoreでは、指定するインスタンスタイプの組み合わせによってスコアが大きく変わります。 そこで、以下の4パターンを比較しました。

セットA: 2xlargeのみ(現状に近い構成)

  • c7g / m7g / r7g系の2xlarge

セットB: xlarge + 2xlarge(xlargeを追加したサイズ分散)

  • セットA + xlarge系

セットC: 2xlarge + 4xlarge(4xlargeを追加したサイズ分散)

  • セットA + 4xlarge系

セットD: 世代分散(第7世代 + 第8世代)

  • セットA + c8g / m8g / r8g系

評価に使ったスクリプト(参考)

以下は、実際に評価に使ったスクリプトと実行結果です。

AWS CLIの公式ドキュメントを参考にしつつ、Claude Codeに生成してもらったものをベースに、用途に合わせて微調整しています。

docs.aws.amazon.com

スクリプト

#!/usr/bin/env bash
set -euo pipefail

# ===== 引数チェック =====
if [[ $# -lt 1 ]]; then
  echo "Usage: $0 <TARGET_CAPACITY_UNITS>" >&2
  exit 1
fi

CAP="$1"
REG="ap-northeast-1"
PROFILE="$2"

# ===== 候補セット =====
# A: 2xlarge のみ
TYPES_A=("c7g.2xlarge" "c7gd.2xlarge" "c7gn.2xlarge" "m7g.2xlarge" "m7gd.2xlarge" "r7g.2xlarge")
# B: xlarge + 2xlarge
TYPES_B=("c7g.xlarge" "c7gd.xlarge" "c7gn.xlarge" "m7g.xlarge" "m7gd.xlarge" "r7g.xlarge" "${TYPES_A[@]}")
# C: 2xlarge + 4xlarge
TYPES_C=("c7g.4xlarge" "c7gd.4xlarge" "c7gn.4xlarge" "m7g.4xlarge" "m7gd.4xlarge" "r7g.4xlarge" "${TYPES_A[@]}")
# D: 世代分散
TYPES_D=("c7g.2xlarge" "c7gd.2xlarge" "c7gn.2xlarge" "m7g.2xlarge" "m7gd.2xlarge" "r7g.2xlarge" "c8g.2xlarge" "c8gd.2xlarge" "m8g.2xlarge" "m8gd.2xlarge" "r8g.2xlarge")

ZONENAMES=("ap-northeast-1b" "ap-northeast-1c" "ap-northeast-1d")

# AZ名→AZ ID を返す関数
az_id_of() {
  local zn="$1"
  aws --profile "$PROFILE" ec2 describe-availability-zones \
    --region "$REG" \
    --zone-names "$zn" \
    --query 'AvailabilityZones[0].ZoneId' \
    --output text
}

# スコア取得(AZ単位)
score_block() {
  # $@: インスタンスタイプの可変長引数
  aws --profile "$PROFILE" ec2 get-spot-placement-scores \
    --region "$REG" \
    --single-availability-zone \
    --region-names "$REG" \
    --target-capacity "$CAP" \
    --target-capacity-unit-type units \
    --instance-types "$@" \
    --query 'SpotPlacementScores[?Region==`ap-northeast-1`].[AvailabilityZoneId,Score]' \
    --output text
}

# ===== メインループ =====
for SET in A B C D; do
  # 間接展開で TYPES_A/B/C 配列を参照
  MAPFILE_VAR="TYPES_${SET}[@]"
  # shellcheck disable=SC1083,SC2086
  TYPES=("${!MAPFILE_VAR}")

  echo "===== セット ${SET} ====="
  printf "候補タイプ: "
  printf "%s " "${TYPES[@]}"
  printf "\n"

  scores="$(score_block "${TYPES[@]}")"

  for zn in "${ZONENAMES[@]}"; do
    azid="$(az_id_of "$zn")"
    # awkで該当AZ IDのスコアだけ表示
    # (見つからない場合は空行にならないよう NA を出す)
    line="$(awk -v id="$azid" '$1==id{print $0}' <<< "$scores" || true)"
    if [[ -n "$line" ]]; then
      # line: "<AZID>\t<SCORE>"
      printf "%s (%s): %s\n" "$zn" "$azid" "$(awk '{print $2}' <<< "$line")"
    else
      printf "%s (%s): NA\n" "$zn" "$azid"
    fi
  done
  echo
done


4. 評価結果

上記のスクリプトを実行した結果は以下のとおりです。

# 150台要求した場合のAZ毎のスコアを算出する
$ ./get-spot-placement-scores.sh 150 <profile>
===== セット A =====
候補タイプ: c7g.2xlarge c7gd.2xlarge c7gn.2xlarge m7g.2xlarge m7gd.2xlarge r7g.2xlarge
ap-northeast-1b (apne1-az4): 2
ap-northeast-1c (apne1-az1): 3
ap-northeast-1d (apne1-az2): 2

===== セット B =====
候補タイプ: c7g.xlarge c7gd.xlarge c7gn.xlarge m7g.xlarge m7gd.xlarge r7g.xlarge c7g.2xlarge c7gd.2xlarge c7gn.2xlarge m7g.2xlarge m7gd.2xlarge r7g.2xlarge
ap-northeast-1b (apne1-az4): 8
ap-northeast-1c (apne1-az1): 8
ap-northeast-1d (apne1-az2): 5

===== セット C =====
候補タイプ: c7g.4xlarge c7gd.4xlarge c7gn.4xlarge m7g.4xlarge m7gd.4xlarge r7g.4xlarge c7g.2xlarge c7gd.2xlarge c7gn.2xlarge m7g.2xlarge m7gd.2xlarge r7g.2xlarge
ap-northeast-1b (apne1-az4): 9
ap-northeast-1c (apne1-az1): 9
ap-northeast-1d (apne1-az2): 7

===== セット D =====
候補タイプ: c7g.2xlarge c7gd.2xlarge c7gn.2xlarge m7g.2xlarge m7gd.2xlarge r7g.2xlarge c8g.2xlarge c8gd.2xlarge m8g.2xlarge m8gd.2xlarge r8g.2xlarge
ap-northeast-1b (apne1-az4): 5
ap-northeast-1c (apne1-az1): 6
ap-northeast-1d (apne1-az2): 5

スコア結果(150units / AZ別)

  • セットA(2xlargeのみ)

    • 1b: 2 / 1c: 3 / 1d: 2
  • セットB(xlarge + 2xlarge)

    • 1b: 8 / 1c: 8 / 1d: 5
  • セットC(4xlarge + 2xlarge)

    • 1b: 9 / 1c: 9 / 1d: 7
  • セットD(世代分散:第7 + 第8 / 2xlarge固定)

    • 1b: 5 / 1c: 6 / 1d: 5

スコアは10に近いほど確保しやすいことを示しますが、推奨値であり確保を保証するものではありません。ただし、構成間の相対比較としては十分に意味があります。

結論(この結果から言えること)

今回の結果から得られた結論は次のとおりです。

  • 現状に近い「2xlarge固定(セットA)」では、150unitsの要求が通らない可能性が高い

    • 全AZでスコアが2〜3と低く、ピーク時に「必要な台数までスケールできない」リスクが示唆されます。
    • つまり、現状はAMDのスポットインスタンスをフォールバックとして持っている構成に支えられていると言えます。
  • 世代分散(セットD)は改善はするが、期待したほど"強いカード"にはならなかった

    • 第7世代に加えて第8世代も候補に入れたセットDは、1b: 5 / 1c: 6 / 1d: 5でした。
    • セットAからの改善は確認できるものの、スコアは5〜6に留まり、世代分散だけで「安心して150unitsを満たせる」と言えるほどではありませんでした。
    • この結果から、ボトルネックは世代というよりも2xlargeに要求が集中していることに寄っている可能性が高いと考えられます。
  • サイズ分散は効果が大きく、特に4xlargeを混ぜたセットCが最も安定してスコアが高い

    • セットB(xlarge + 2xlarge)でも1b/1cは8まで改善しましたが、1dが5とまだ弱めでした。
    • セットC(4xlarge + 2xlarge)では1dも7まで改善し、3AZ全体としての不安が小さくなりました。
    • つまり今回の条件では、世代分散よりもサイズ分散の方が確保確率を押し上げるレバーとして効きやすいことが分かりました。

最後に

最終的に評価結果を受けて「4xlargeのインスタンスタイプで構成されるノードグループ」を新たに新設し、段階的にAMDインスタンスのノードグループと置き換える、という対応をしました。

今回一番勉強になったのは、直感的に世代やサイズを分散させることでインスタンスの枯渇リスクを減らせるだろう、と考えていたけど、実際にスコアリングすることで、より自信を持った意思決定ができたということです。

あと、AWSが公式に説明している通り、Spot Placement Scoreはあくまで推奨値に過ぎず、スポットの確保を保証してくれるものではありません。実際、同じ条件で数時間おきに取得したところ、スコアが±1〜2程度変動することもありました。 なので、実運用では、実際のピークタイムにどのノードグループがどれぐらい増えたのかというのをDatadogなどで観察しつつ調整していくことが必要だと思います。

というわけで、ARMスポットが十分に確保できるのか?という問いに対しては、

  • 枯渇しない保証はない
  • でも、構成次第で"通りやすさ"は底上げできる
  • それを事前に見積もるのにSpot Placement Scoreが便利

というのが、今回のまとめです。

もし同じように「ARMへの統一を検討しているけど、スポットの可用性が心配」という方がいれば、ぜひSpot Placement Scoreを試してみてください。