本記事は Amazon EKS Advent Calendar の20日目です!(何故人は直前にならないと書き始めないのでしょうか。私だけですかね。すいません)
少し前に「AWS が Kubernetes でのスポットインスタンスノードの自動ドレイニングをサポート」というリリースがありました。本記事では、リリース内で触れている「AWS Node Termination Handler」を中心に、大幅なコスト削減に利用できるスポットインスタンスを Amazon EKS と組み合わせて使うことについて説明します。結構簡単なのでぜひ試してみて下さい。(スポットインスタンス初心者大歓迎です)
スポットインスタンス とは
詳細な説明は 公式ドキュメント を参照して頂ければと思いますが、簡潔に言うとスポットインスタンスとは 制約 を受け入れることで大幅な割引価格で EC2 インスタンスを利用できる機能(起動方法)です。
こちらは一例ですが、直近(2019-12-19執筆時点)の c5.large のスポット価格はオンデマンドの 約31% です。👇👇👇(大体、7割引ということですね!!)
※ スポット価格はインスタンスタイプやアベイラビリティゾーンによって変わります。
制約とは
スポットインスタンスは AWS クラウド内の使用されていない余剰キャパシティーを利用することで、大幅に割り引かれた価格で EC2 インスタンス利用することが出来る機能ですが、これはクラウド内利用状況に応じて価格が変動する、場合によってはインスタンスが Terminate されることを意味します。
したがって、利用の際にはインスタンスの Terminate をいかにハンドリングするかが重要となってきます。
具体的には Terminate の2分前に Amazon EventBridge (CloudWatch Events) とインスタンスメタデータ に通知が来るので、その情報を元にインスタンス上で稼働している Pod を他のインスタンス上に移す必要があります。
今まではこの様な実装を各自行っていた(OSSツールもあります)のですが、AWS Node Termination Handlerとしてオフィシャルに提供されるようになったので、より簡単にスポットインスタンスを利用できるようになりました。
Let'a try!
クラスターの起動
まずは EKS クラスターを準備します。既に検証用のクラスターがある場合は飛ばしていただいて構いません。
今回はeksctl
を利用します。(クラスター名やリージョンはお好きなものに変更して下さい)
eksctl create cluster --name=spot-example --nodes=3 --managed --region=ap-northeast-1
クラスターの作成が終わったら、オンデマンドで起動したことが分かるようにラベルを付与します。(スポットインスタンスの利用には直接的には不要ですが、今回は分かりやすくするために付与します。)
kubectl label nodes --all 'lifecycle=OnDemand'
終わったらワーカーノードを確認します。--selector=lifecycle=OnDemand
オプションで指定したラベルが付与されたワーカノードのみを表示することが出来ます。
kubectl get nodes --show-labels --selector=lifecycle=OnDemand
スポットインスタンスの Node Group の作成
まずは下記のような定義ファイルを作成します。
apiVersion: eksctl.io/v1alpha5
kind: ClusterConfig
metadata:
name: spot-example
region: ap-northeast-1
nodeGroups:
- name: ng-spot
labels:
lifecycle: Ec2Spot # Spot であることが分かるようにしています!
taints:
spotInstance: true:PreferNoSchedule
minSize: 2
maxSize: 2
instancesDistribution: # 少なくとも2つのインスタンスタイプが必要です
instanceTypes:
- m5.large
- c5.large
- r5.large
onDemandBaseCapacity: 0
onDemandPercentageAboveBaseCapacity: 0 # 一部オンデマンドで起動することも出来ます。
spotInstancePools: 2
終わったら Node Group を作成します。完了したら同様に kubectl get nodes
コマンドで確認します。
eksctl create nodegroup -f ./ng-spot.yaml
kubectl get nodes --show-labels --selector=lifecycle=Ec2Spot
AWS Node Termination Handler とは
ここからが本題です。前述の通り、スポットインスタンスはユーザーの意図しないタイミングで Terminate されるため、その2分前に Amazon EventBridge (CloudWatch Events) とインスタンスメタデータ にくる通知をハンドリングして Pod の退避を行う必要があります。
これを行ってくれるのが AWS Node Termination Handler です。AWS Node Termination Handler はインスタンスメタデータをチェックし、Terminate 予定のあるワーカノードを退役させます。具体的な動きは下記のような感じです。
- AWS Node Termination Handler をクラスターに展開する。 (
kubectl apply
) - Daemonset として Pod が各ワーカーノードで実行される
- Pod に含まれるコンテナが インスタンスメタデータ を定期的にチェックする
- Terminate の2分前があると、ワーカノードをクラスターから退役させる
- Kubernetes クラスターは退役予定のワーカノード上で動いている Pod を他のワーカーノードに移動させる
AWS Node Termination Handler の実行
下記コマンドで Daemonset を宣言します。
kubectl apply -k "https://github.com/aws/aws-node-termination-handler/config/base?ref=master"
kubectl get daemonsets
で確認します。
kubectl --namespace=kube-system get daemonsets
NAME DESIRED CURRENT READY UP-TO-DATE AVAILABLE NODE SELECTOR AGE
aws-node 5 5 5 5 5 <none> 139m
kube-proxy 5 5 5 5 5 <none> 139m
node-termination-handler 5 5 5 5 5 <none> 19s
node-termination-handler が 5Pods 動いているかと思いますが(既存のクラスタで試されている場合はこの限りではありません)、OnDemand: 3, Spot:2 にも関わらず、全ワーカーノードで node-termination-handler が動いてしまっているためあまり効率的ではありません。
そこで予め付与しているラベルを利用して、lifecycle: Ec2Spot
のワーカノードのみに制限するようにしましょう。作業は下記コマンドで行います。
kubectl apply "https://github.com/aws/aws-node-termination-handler/config/overlays/spot-node-selector/daemon-set-spot-node-selector.yaml?ref=master"
ソースコードを見て頂ければ分かりますが、上記 base の定義ファイルに加えて下記のようなnodeSelector
の設定が入っています。仮に自社でスポットインスタンスに異なるタグを付与している場合は、Ec2Spot
を変更する必要があります。
apiVersion: apps/v1
kind: DaemonSet
metadata:
name: node-termination-handler
spec:
template:
spec:
nodeSelector:
lifecycle: Ec2Spot # 👈 任意のラベルに変更可能
改めて確認してみると、node-termination-handler が起動する Node が lifecycle=Ec2Spot
に制限され、Pod 数がスポットインスタンス数に一致していることが確認できるかと思います。
kubectl --namespace=kube-system get daemonsets
NAME DESIRED CURRENT READY UP-TO-DATE AVAILABLE NODE SELECTOR AGE
aws-node 5 5 5 5 5 <none> 151m
kube-proxy 5 5 5 5 5 <none> 151m
node-termination-handler 2 2 2 2 2 lifecycle=Ec2Spot 12m
簡単ですね。
※ なお今回は Node Selector の説明をするために分けて kubectl apply
しましたが、
kubectl apply "https://github.com/aws/aws-node-termination-handler/config/overlays/spot-node-selector/daemon-set-spot-node-selector.yaml?ref=master"
だけで問題ありませんので、その点だけご注意下さい。
AWS Node Termination Handler だけで「いい感じに」使えるのか?
クラスター側の設定は上記で問題ありませんが、Pod の処理が中断された場合のハンドリングは別途アプリケーションレベルで行う必要があります。
Web の UI や API を提供している場合は、一定時間内に Connection を終了し別の Pod にロードバランスされるように適切に設定したり、ジョブワーカーの場合は処理途中で停止した Pod の掴んでいたキューを他のPod が再処理しても問題が起きないように作るといった感じです。
このあたりはコンテナ化する際にクリアされているケースが多いと思われますが、スポットインスタンス利用の際は中断時の挙動を見直すことをおすすめします。
まとめ
今まで各社個別にスポットインスタンスの退役処理を実装されていたかと思いますが、AWS Node Termination Handler の登場でより簡単にスポットインスタンスを利用できるようになりました。
AWS Fargate の Amazon EKS サポートもかなり熱いアップデートですが、より高いコスト効率を求める方はぜひスポットインスタンスを試してみて下さい。
なお、今回は Amazon EKS, eksctl
前提ででも環境を作成しましたが、kops
等で構築した Self-hosted な Kubernetes でも利用可能です。