kube-awsにノードプール機能が追加されました

このエントリーは[Kubernetes Advent Calendar 2016](http://qiita.com/advent-calendar/2016/kubernetes)の5日目の記事です。

4日目はkoudaiiiさんの[CronJob(ScheduledJob) - Qiita](http://qiita.com/koudaiii/items/000724c27e65be2d4484)でした。これKubernetesのキラーフィーチャーの一つな気がするのですが、あんまり話題になってない気がするのでみんなもっと知ってほしい。分散cronがビルトインなのはKubernetesだけですよ!(例えばMesosのChronosはまだビルトインじゃないですよね?

5日目のこの記事では、kube-awsに最近追加されたNode Pools(ノードプール)という機能について書きます。

## TL;DR;

[kube-aws v0.9.2-rc.2](https://github.com/coreos/kube-aws/releases/tag/v0.9.2-rc.2)から、GKEにもある「ノードプール」という便利機能が追加されて、コスト圧縮やMultiAZ・ノードオートスケーリング(Podではなく)の両立がしやすくなりました。

## kube-awsとは

kube-awsはKubernetesクラスタをCloudFormationスタックとして構築・管理するためのOSSです。
現在はH/A構成のKubernetesクラスタの作成、更新、削除に対応しています。

手前味噌ですが、kube-aws自体の詳しい説明や使い方については、[CloudFormationでCoreOS + Kubernetesクラスタをつくるkube-awsのまとめ - Qiita](http://qiita.com/mumoshu/items/e2492513c24cae928bbd)を読んでみてください:wink:

## ノードプールとは

GKEに実装されている、一つのKubernetesクラスタに複数タイプのWorkerノードを混在させるための機能です。
GCPJブログの記事がわかりやすいので、抜粋させていただきます。

> 異種構成のクラスタを作成することは、これまでは非常に困難でした。
従来、クラスタを作るときには、ノードのマシン タイプやディスク サイズなどのオプションを選択できました。しかし、それがすべてのノードに適用されるため、クラスタは同種的になっていたのです。
このたび GA リリースされた GKE の新機能であるノード プールは、この問題を解決します。ノード プールは同じ構成のマシンのコレクション、すなわち “プール” です。これにより、クラスタはすべてのノードが同じでなければならない画一的なものではなくなり、複数のノード プールを持てるようになって、従来よりも細かくニーズに対応できるようになりました。
> [Google Cloud Platform Japan 公式ブログ: Google Container Engine（GKE）に追加されたノード プールの使い方](http://googlecloudplatform-japan.blogspot.jp/2016/06/google-container-enginegke.html)

GKEではノードプール毎に以下の項目がカスタマイズできます。

* マシンタイプ(AWSでいうインスタンスタイプ)
* ゾーン(AWSでいうAZ)
* ノード数

クラスタに０個以上のノードプールを追加することができて、各ノードプールにそれぞれ１個以上のノードが作成されます。ノードプール内のインスタンスは、同じマシンタイプ、ゾーンなどが割り当てられます。

ノードプールがあると何がうれしいのかというと、クラスタ全体で無駄にするリソースが減るということと、H/Aとオートスケーリングの両立、の２点が達成できることです。

### クラスタ全体で無駄にするリソースが減る

例えば、メモリを1GB要求するPodと10GB要求するようなPodが混在するような環境の場合、ノードプールがないときは最低でもメモリを10GB以上積んでいるノード（GCPだとマシンタイプ)を選択する必要がありました。それ未満のメモリしか積んでいないノードを選択してしまうと、10GB要求するPodをスケジュールできないからです。

しかし、そうすると例えばそれぞれのPodのレプリカ数が１でいい・・・という場合、無駄なりソースが生まれてしまいます。例えばメモリを10GB搭載しているノードを採用したとすると(実際はそんな中途半端なメモリサイズのマシンタイプはなさそうですが)、要求メモリは合計11GBですがノード数は2となってしまい、20-11=メモリ9GB分に支払っているコストは無駄、ということになってしまいます。

ノードあたりのメモリサイズ: 10GB
要求メモリ: 10+1 GB
確保メモリ: 10*2 GB
無駄になったメモリ: 9 GB (:point_left: 減らしたい)

これを解決する一つの方法は、Podによってスケジュール先のノードプールをわけることです。

例えば、2GBのメモリを搭載しているノードからなるノードプールと、10GBのメモリを搭載しているノードからなるノードプールを用意します。1GB要求するpodは前者のプールに、10GBのpodは後者のプールにスケジュールするようにすれば、先程の例だとクラスタ全体で要求されるリソースはメモリ11GBに対して、確保するりソースは12GBになります。要求11GBに対して確保12GBなので、無駄が1GBになります。

プール1:
ノードあたりのメモリサイズ: 2GB
要求メモリ: 1 GB
確保メモリ: 2 GB
無駄になったメモリ: 1 GB

プール2:
ノードあたりのメモリサイズ: 10GB
要求メモリ: 10 GB
確保メモリ: 10 GB
無駄になったメモリ: 0 GB

合計で無駄になったメモリが9GBから1GBに減った :tada:

極端な例ではありますが、ノードプールがないとこれが全くできませんでした。

// なお、実装としては、それぞれのノードを区別するためのlabelをつけて、podのnode selectorを使ってスケジュール先のノードを選択させます。

### H/Aとオートスケーリングの両立

実は、ノードプールがないとこの２つは両立できないのです。

KubernetesにはUbernetes Liteという機能があります。Kubernetesクラスタに複数AZのworkerノードを混ぜると、podを分散してスケジュールしてくれる関係で、結果的にpodがMulti-AZ構成になる、というものです。一つのAZが落ちたとしても、クラスタで稼働しているpodが全て落ちたりはせずにすみます(ただし、replicasetでreplica数は最低2にしましょう!)

一方で、Kubernetesクラスタのノードのオートスケーリングに使う[cluster-autoscaler](https://github.com/kubernetes/contrib/blob/master/cluster-autoscaler/)というものがあります。これの仕組みは、Kubernetesクラスタ全体でpendingになっているpodを発見したら、それが「スケジュールされるように必要なノードを追加する」です。おっ、かしこい！と思うじゃないですが。しかし、これには落とし穴があります。READMEに詳しく書かれているので抜粋すると、

> The autoscaling group should span 1 availability zone for the cluster autoscaler to work. If you want to distribute workloads evenly across zones, set up multiple ASGs, with a cluster autoscaler for each ASG. At the time of writing this, cluster autoscaler is unaware of availability zones and although autoscaling groups can contain instances in multiple availability zones when configured so, the cluster autoscaler can't reliably add nodes to desired zones. That's because AWS AutoScaling determines which zone to add nodes which is out of the control of the cluster autoscaler. For more information, see https://github.com/kubernetes/contrib/pull/1552#discussion_r75533090.
> :link: https://github.com/kubernetes/contrib/blob/master/cluster-autoscaler/cloudprovider/aws/README.md

例えば、`1 ノード郡 = 1 ASG = 2 AZ1というセットアップにしたときを考えてみます。

* myappというreplicasetがmyapp-1~3というpodをスケジュールしている状態(replicas=3)
* ASGに２つのAZが割り当てられている
* ASGにnode1とnode2がいる
* それぞれnodeのキャパはいっぱい

この状態で、myappのレプリカ数を増やしてみます。

|   ノード名 | AZ | Pods |
|:---------:|:-:|:-:|
| node1    | ap-northeast-1a  | myapp-1, myapp-3  |
| node2    | ap-northeast-1b  | myapp-2, anotherapp-1  |

pendingになっている: myapp-4

nodeのキャパがいっぱいなのでmyapp-4はpendingになりました。

では、ノードを増やします。
ん？どちらのAZのノードを増やすのが理想でしょうか?

答えは、1bです。myappは1aに2つ、1bに1つというようにアンバランスにスケジュールされています。AZに均等にデプロイする(Ubernetes Lite)ために、Kubernetesのスケジューラは1bにpodをスケジュールしようとするからです。

改めてノードを増やしましょう。まず、1bにノードを追加にするため、ASGのDesired Capacityを増やして…。

ん？
Desired Capacityを増やしたら、1aと1bのどちらにノードが追加されるのでしょうか?

未定義なんです…。これはASGの制限?仕様です。場合によって1aが追加されることもあるし、1bが追加されることもあります。もし1aに追加された場合、1bにスケジュールされたがっているpodはpendingのままなので、もう一度cluster-autoscalerによりDesired Capacityが+1されます。今度はノードをAZでバランスするため1bにノードが追加されるでしょう。pendingになってから、必要なノードが揃うまで２回のイテレーションが必要でした。これがREADMEに書かれている"can't reliably add nodes to desired zones"ということです。

ノードプールがあれば、ノードプール毎に単一のAZに割り当てるようにしておくことで、この問題を回避できます。podに割り当てられているノードのASGにAZが２つ割り当てられているとunreliableになりますが、ASGが一つになっていたらpendingになっているpodのノードが所属しているASGを+1にすれば確実に足りないAZにノードが追加されるからです（長い)

## ノードプールまとめ

コストを圧縮したり、もしくはMulti-AZ構成でノードのオートスケーリングを行いたい場合は必須な機能です。

## kube-awsのノードプールとは

コスト圧縮やMulti-AZとノードのオートスケーリング両立をサポートできるように、GKEと似たような同名の機能を実装したものです。

2016/12/5時点の仕様は以下のとおりです。

* メインとなるクラスタと、各ノードプールは個別のCloudFormationスタックになる
  * ただし、クラスタ間で共有する必要があるAWSリソース（特定のSecurityGroupやVPC、RouteTableなど)は[CloudFormationのクロススタックリファレンス機能](http://docs.aws.amazon.com/ja_jp/AWSCloudFormation/latest/UserGuide/walkthrough-crossstackref.html)を利用します
  * 結果的に、ノードプールを消さないと、メインとなるクラスタは消せない(事故防止
* ノードプール毎にメインクラスタのworkerノードと異なる設定ができる
* ノードプール毎にカスタマイズできるもの
  * Kubeletバージョン
  * CoreOSバージョン
  * KMSキー
  * KeyPair名
  * インスタンスタイプ
  * EBSボリュームのサイズ、タイプ、IOPS
  * cloud-config

2016/12/5時点でできないことは以下のとおりです。

* 自動的にlabelをつける
  * labelをつけないとpodをノードプールに振り分けられないので、コスト削減の用途だとそのままは使えません
  * cloud-configを変更して、kubeletがノードを登録する前にラベルをつけるようにする必要があります
  * // GKEだとデフォルトでいくつかlabelはつくが、カスタムラベルは手動でつける必要があったはず
* 自動的にtaintをつける
  * labelと似ているけど逆の機能でtaintというものがあります
  * labelはノードを選択するために使いますが、taintはノードをデフォルトで除外するために使います
  * 特定ノードプールは常に開けておきたい、特定pod専用にしたい・・・というときにlabelより便利です
  * // GKEでもできないはず
* SpotインスタンスやSpot Fleetを使う
  * ただしSpot Fleetに関しては[ProposalとPRが出ています](https://github.com/coreos/kube-aws/issues/112)
* ノードプール内のインスタンスのRollingUpdate（インスタンスタイプなどを変更して無停止で1台ずつ入れ替えかえる機能)
  * メインクラスタに対しては`kube-aws update`というコマンドでできますが、ノードプール用はまだありません

## kube-awsのノードプールを作成する手順

メインクラスタを作成するための`kube-aws (init|render|up)`と同じコマンドが、`node-pools`サブコマンド配下に追加されているので、それを使います。

```
$ kube-aws init　--cluster-name ${メインクラスタ名} \
      --availability-zone ${AZ名} \
      --key-name ${EC2キーペア名
      --kms-key-arn ${KMSキーARN}
$ kube-aws render
$ kube-aws up
# ここまででメインクラスタが起動して、cluster.yamlが生成されている状態になる。

# ここからノードプール作成
$ kube-aws node-pools init --node-pool-name ${ノードプール名} \
      --availability-zone ${AZ名} \
      --key-name ${EC2キーペア名
      --kms-key-arn ${KMSキーARN}
$ kube-aws node-pools render --node-pool-name ${ノードプール名}
$ kube-aws node-pools up
```

## まとめ

* kube-awsにもGKEと似たようなノードプール機能が追加されました :tada:
* ノードプールは[kube-aws v0.9.2-rc.2](https://github.com/coreos/kube-aws/releases/tag/v0.9.2-rc.2)からサポートされています。
* Kubernetesをコスト削減の目的で導入したい場合や、Multi-AZ・ノードオートスケーリング(Podではなく)の両立をしたい場合にもkueb-awsが利用できるようになりました