OpenShift AWS IPIでSpot Instanceを利用する

Red HatでOpenShiftのサポートをしているid:nekopです。

OpenShift 4.5ではAWSのSpot Instanceを利用することが可能になっているので、普段テストなどに使っているAWSのクラスタにSpot Instanceを適用してコスト削減してみました。テスト目的のクラスタなので、サービスを提供しているものではなく、営業時間内しか利用しません。Spot Instanceは意図しないタイミングで中断されるインスタンスですが、このテスト目的ユースケースでは中断されても特に大きな影響はない、という前提です。

docs.openshift.com

会社では基本的にTokyo (ap-northeast-1)リージョンでm5.xlarge (4vCPU 16GB mem)を利用していますが、インスタンスコストが$0.25 per hourで、3 masters 3 workers構成で営業時間(8時間仮定)のみの起動でインスタンスコストの合計は $0.25 * 8 hours * 20 days * 6 hosts == $240.0 というお値段となります。インスタンスコスト以外にELB, NAT gateway, EBSが一日$7程度課金されるので、営業時間外はインスタンスを停止する運用でも、合計一ヶ月$440程度になります。まったく停止しないでフル稼働の場合は$1080です。

利用するSpot Instanceについて料金履歴, 料金表, スポットインスタンスアドバイザーなどを事前に確認しましょう。m5.xlargeのSpot料金履歴を見ると$0.07程度なので、以下の例ではmaxPrice: 0.08で設定します。Frequency of interruptionは10-15%となっています。WorkerホストをSpot Instanceにスイッチすると$0.08 * 8 hours * 20 days * 3 hosts == $38.4ということで元々$120だったインスタンスコストが$40に圧縮できます。$80の節約ですね。MasterホストはSpot Instanceにできないので引き続き$120かかります。営業時間のみインスタンス起動で一ヶ月合計は$360です。

Spot Instanceを設定していきましょう。クラスタを作成後、MachineSetにSpot Instance設定を反映してoc delete machineで既存のOn-Demandで作成されたWorkerをSpot Instance設定でリビルドしていきます。

for ms in $(oc -n openshift-machine-api get machineset -o name); do
  oc -n openshift-machine-api patch $ms --type json -p '[{"op":"add","path":"/spec/template/spec/providerSpec/value/spotMarketOptions","value":{"maxPrice":"0.08"}}]'
done

oc create -f - <<EOF
apiVersion: machine.openshift.io/v1beta1
kind: MachineHealthCheck
metadata:
  name: worker
  namespace: openshift-machine-api
spec:
  selector:
    matchLabels:
      machine.openshift.io/cluster-api-machine-role: worker
      machine.openshift.io/cluster-api-machine-type: worker
  unhealthyConditions:
  - type:    "Ready"
    timeout: "360s" 
    status: "False"
  - type:    "Ready"
    timeout: "360s" 
    status: "Unknown"
  maxUnhealthy: "100%"
EOF

oc -n openshift-machine-api get machine
oc -n openshift-machine-api delete machine MACHINE # ひとつずつリビルド

Spot Instanceはone-shotタイプでstopはできず、終了させるにはterminateする必要があるので、停止手順はMasterホストのstopとSpot InstanceのWorkerホストのterminateとなります。MachineHealthCheckを入れておくことで、クラスタを再開するとき、MasterをstartしてMachineConfigOperatorが正常に動作した時点でWorkerホストの自動修復がトリガーされてクラスタが正常な状態となります。Workerを新規作成するので開始に5分程度かかります。

クラスタを毎回再インストールしたほうがAWSコストは安くなりますが、クラスタを再インストールすると40分程度かかる、40分ほど使えないけど課金される時間が発生する、毎回必要な初期化や追加オペレータのインストールの手間がかかる、毎日決まった時間に絶対使うというわけでもないので無駄な課金を避けようとすると自動クラスタ作成などの自動化も仕込みづらい、すぐに必要というときに40分も待てないケースがある、といった事情を考えると、クラスタを毎回再インストールするというのも様々なオーバーヘッドがあります。端的に言うとだるい。とてもだるい。停止していても一日$7かかりますが、人間がクラスタ再作成にかかわるだるい作業を行うというコストは$20くらいはありそうなので、使用頻度が高い場合はクラスタ再作成はしないほうがオトクです。

最後にAWS cliを利用した起動停止スクリプトを添付しておきます。

#!/bin/bash

# Example: ocp-aws start cluster-blue
# 
# This script needs aws cli:
# $ curl "https://awscli.amazonaws.com/awscli-exe-linux-x86_64.zip" -o "awscliv2.zip"
# $ unzip awscliv2.zip
# $ ./aws/install -i ~/.local/aws-cli -b ~/.local/bin
# 
# You can put this script under ~/.local/bin along with aws command.
#
# To enable automated shutdown using cron:
# $ crontab -e
# 0 19 * * * $HOME/.local/bin/ocp-aws stop cluster-blue
# 

OPERATION=$1
CLUSTER_PREFIX=$2

if [ "$CLUSTER_PREFIX" == "" ]; then
  echo "Usage: ocp-aws COMMAND PREFIX"
  echo "COMMAND: start, stop"
  exit 1
fi

AWS_COMMAND=$HOME/.local/bin/aws
REGION=ap-northeast-1

case "$OPERATION" in
"start")
  $AWS_COMMAND ec2 start-instances --region ${REGION} --instance-ids $($AWS_COMMAND ec2 describe-instances --filters "Name=tag:Name,Values=${CLUSTER_PREFIX}-*" "Name=instance-state-name,Values=stopped" --query "Reservations[*].Instances[*].InstanceId" --region ${REGION} --output text) ;;
"stop")
  # Terminate spot instances then stop ondemand instances
  $AWS_COMMAND ec2 terminate-instances  --region ${REGION} --instance-ids $($AWS_COMMAND ec2 describe-instances --filters "Name=tag:Name,Values=${CLUSTER_PREFIX}-*" "Name=instance-state-name,Values=running" "Name=instance-lifecycle,Values=spot" --query "Reservations[*].Instances[*].InstanceId" --region ${REGION} --output text)
  $AWS_COMMAND ec2 stop-instances  --region ${REGION} --instance-ids $($AWS_COMMAND ec2 describe-instances --filters "Name=tag:Name,Values=${CLUSTER_PREFIX}-*" "Name=instance-state-name,Values=running" --query "Reservations[*].Instances[*].InstanceId" --region ${REGION} --output text) ;;
*) echo "Unknown operation" ;;
esac

* 各記事は著者の見解によるものでありその所属組織を代表する公式なものではありません。その内容については非公式見解を含みます。