地味に便利な DaemonSet というのがありまして、1Node に 1Pod 起動する仕組みになっています。
これを使って、Node単位でやりたい処理を仕込むことができます。例えば……恒例のスポットインスタンスの強制Terminate対策とか、してみましょうそうしましょう。
DaemonSetとは
一応、説明は軽く見たほうがよいのですが、Nodeの起動後に確実に1pod起動されるということで、EKSの場合は aws-node という名前のPodが起動します。が、こいつに関する詳しい公式説明が見つからないのが、ちょっと残念なところ。
1 2 3 |
$ kubectl get pod --all-namespaces | grep aws-node kube-system aws-node-2fz7g 1/1 Running 0 40m kube-system aws-node-74lzn 1/1 Running 0 40m |
前に Service で NodePort を作りましたが、それは kube-proxy として活躍しております。
1 2 3 |
$ kubectl get pod --all-namespaces | grep kube-proxy kube-system kube-proxy-h6t7j 1/1 Running 0 40m kube-system kube-proxy-qc7js 1/1 Running 0 40m |
DaemonSet としてはこんな感じで、今回は node-manager という名前で、自分がやりたいことを仕込む Pod を起動していきます。
1 2 3 4 5 |
$ kubectl get daemonset --all-namespaces NAMESPACE NAME DESIRED CURRENT READY UP-TO-DATE AVAILABLE NODE SELECTOR AGE default node-manager 2 2 2 2 2 <none> 5d8h kube-system aws-node 2 2 2 2 2 <none> 5d8h kube-system kube-proxy 2 2 2 2 2 <none> 5d8h |
他のスポットインスタンスを扱う方法
先に、既存の EKS + Spot 関連の記事を記載しておきます。- Using Spot Instances with EKS :: Amazon EKS Workshop
- Run your Kubernetes Workloads on Amazon EC2 Spot Instances with Amazon EKS | AWS Compute Blog
- kube-aws/kube-spot-termination-notice-handler: A Kubernetes DaemonSet to gracefully delete pods 2 minutes before an EC2 Spot Instance gets terminated
- Kubernetes: 複数の Node を安全に停止する (kubectl drain + PodDisruptionBudget) – Qiita
AWS公式で紹介している方法として、同様に DaemonSet を使っているものがあります。この中身を追っていくと、kubectl drain によって、terminate 直前の Node から Pod を退避する手法をとっていることがわかります。
これはこれで、採用するべき構造の場合があると思うので、選択肢の1つとして知っておきたいところです。
これまでのスポット対策の流れ
私の場合、既存の仕組みを使いまわしたい意図があったため、独自の路線をいくことにしました。といっても、そう変なことをするわけでもありません。最古の手法
元々、EC2 をそのまま利用する構成において、その EC2 内にスポット検知用のデーモンを常駐させ、検知したら(当時は)ELBから自身を外すなど、安全な状態にしてから死を待つ、という処理をしていました。悪い内容なわけではないですが、それなりに煩雑な処理をイメージに組み込むのは、開発・運用上において非常に気持ち悪いものがあり、機をみて改善することにしました。
現在の手法
EC2 にデーモンを常駐させるのは変わりませんが、検知後は Lambda を非同期で実行して終わり、というように変更しました。Lambda には自身の instance-id を渡し、Lambda が ALB TargetGroup から探して外したり、監視サービスの該当ホストを無効にする、といった処理を担当するようになりました。
これにより、BASH じゃなく Python になって書きやすくなり、
独自Lambdaフレームワークで開発しやすくなり、
監視機能などに変更があっても Lambda を変更するだけ、
というように綺麗に切り分けられたと思います。
今回のダメな案
スポット対策に必須な機能として、素直に切り込むと、EC2 すなわち Node で検知することになりますが、イメージは公式配布のEKS用を使っています。検知デーモンを仕込むには、別イメージにするか、UserData あたりで汚らしく埋め込むことになりますが、どちらもお断りです。
また、Pod は Node の EC2メタデータ を取得できるので、EC2の時と同じようにスケール用Podにデーモンを仕込むこともできますが、せっかくコンテナなので、サービス用Podに汚れ仕事はさせたくないですよ、と。
そこに、DaemonSet でやればカッコ可愛いじゃない、と降臨するわけです。
DaemonSet でスポット対策
という流れで、旧来のEC2から移植することになりました。イメージ用コード
前に イメージ自動生成 でやったのと同じように、DaemonSet用のイメージを作成します。そのためのレポジトリはこちらです。内容はシンプルで、entrypoint.sh に動かしたいデーモンを書いていて、spot-monitor.sh にはスポットTerminate検知と、Lambda発火が書いてあります。ほぼ、EC2の時のをそのまま移植しました。
もし、他に動かしたいデーモンがあれば、ここの DaemonSet に追加でやらせるのがほどよいと思います。
Terraformコード
DaemonSet のコードはこんな感じ。toleration で全許可して、node_affinity でEKSがNodeに最初から仕込んでいるラベルに合わせています。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 |
resource "kubernetes_daemonset" "node_manager" { count = local.on_eks ? 1 : 0 metadata { name = "node-manager" labels = { app = "manager" } } spec { selector { match_labels = { app = "manager" } } template { metadata { labels = { app = "manager" } } spec { container { name = local.eks_cluster_name image = local.eks_config["daemonset_image"] resources { requests { cpu = "100m" } } } affinity { node_affinity { required_during_scheduling_ignored_during_execution { node_selector_term { match_expressions { key = "beta.kubernetes.io/os" operator = "In" values = ["linux"] } match_expressions { key = "beta.kubernetes.io/arch" operator = "In" values = ["amd64"] } } } } } toleration { operator = "Exists" } } } } depends_on = [aws_eks_cluster.main] } |
Lambdaコード
Lambda は独自フレームワークになっていてコピペできないので、やっている処理を箇条書きで紹介すると- instance-id など Pod から渡せるデータを受け取る
- 全ての ALB TargetGroup から instance-id を探して deregister_targets
- instance-id の EC2 の SecurityGroup を、ほぼ何もない仮Groupに変更することで、監視などの Input が確実にこなくする
- 監視システムAPIへ、該当 EC2 のホストを無効化リクエスト
- SNS に処理内容を整理したメッセージを publish
検知してから 120秒弱の猶予があるので、このくらいは余裕で終わります。
動作確認
適当なNodeを、まぁまぁ落とされやすそうなインスタンスタイプのスポットで起動しておいて、放置しておくと発動し、何箇所かでその確認をすることができます。まずは AutoscalingGroup のアクティビティ履歴で、Terminate の成功を確認できます。そのあとは、たいてい何分かは余剰リソースがないために起動エラーが続きます。
次に、CloudWatch Logs のLambdaログにて、その処理内容を確認できます。ので、Lambdaを書く時は、ログ出力は気持ち多めにやっておくとよいですね。
最後に、SNS に飛ばしたメッセージを、メールなりなんなりで確認できます。
といった具合です。
今回の方法は、ALB前提の構成ではありますが、なかなか綺麗に処理の分別ができているのではないかと思います。
コンテナでスポットを適用するのは、なんとなく気持ち悪い感じがするかもですが、リソースや費用の都合的にはEC2でやるのとあまり変わらないので、今まで使ってきた人は、EKSでも使えるようにすればいいと思うのです。