Kubernetes: コンテナが起動するまでの各コンポーネントの流れ

`kubectl create -f x.yaml`でReplication Controllerを作成しコンテナが起動するまでに、各コンポーネントがどのように動作するかまとめてみました。Kubernetes 1.2時点での情報です。各コンポーネントの概要は、[Kubernetes: 構成コンポーネント一覧](http://qiita.com/tkusumi/items/c2a92cd52bfdb9edd613)をご覧ください。

## シーケンス図

![image](https://qiita-image-store.s3.amazonaws.com/0/100377/88c579ac-5f41-e014-e17c-c0b3995d5f9a.png)

各コンポーネントはAPIサーバーのリソースを監視(watch)して非同期に動作するので、実際のアクセスの方向はコンポーネントからAPIサーバーになります。

## kubectl createによるReplication Controller作成

`kubectl create -f x.yaml`でReplication Controllerを作成を実行すると、内部ではAPIサーバーの[`POST /api/v1/namespaces/{namespace}/replicationcontrollers`](http://kubernetes.io/docs/api-reference/v1/operations/#_create_a_replicationcontroller)というAPIに対して、Replication Controllerの情報をPOSTします。APIサーバーでReplication Controllerのリソースが作成できた段階(コンテナはまだ起動していない)で完了(`created`)が表示されます。

ファイルはyaml, json形式の両方が指定できますが、一度内部オブジェクトに変換しバリデーションを行った上でPOST時にJSON形式にエンコードされます。

下記のように`--v`オプションでログレベルを指定すると、APIへのアクセスをログで追うことができます。

```bash
$ kubectl create --v=10 -f frontend-controller.yaml
```

## APIサーバーの処理

APIサーバーは各コンポーネントからリソースの情報を受け取りetcdに保存します。あくまでリソースの情報のみを管理していてAPIサーバーが直接Nodeやcontrollerに命令することはありません。各コンポーネントはAPIサーバーのリソースを監視(watch)しているので、非同期にイベントが通達されます。

## controllerの処理

kube-controller-managerの立ち上げるコントローラーの一つに、Replicatoin Controllerを管理するReplication Managerがあります。(ややこしいですがReplication Controllerはリソース名です)

Replication ManagerはAPIサーバーの[`GET /api/v1/watch/replicationcontrollers`](http://kubernetes.io/docs/api-reference/v1/operations/#_watch_individual_changes_to_a_list_of_replicationcontroller_2)、[`GET /api/v1/watch/pods`](http://kubernetes.io/docs/api-reference/v1/operations/#_watch_individual_changes_to_a_list_of_pod_2)を使ってReplication Controllerとpodのリソースの変更を監視(watch)しています。

Replication Managerは、指定されたreplica数とアクティブなpod数の差分を計算し、不足または余剰分のPodを生成・削除して調整を行うのが仕事です。

よって、新規でReplication Controllerが作成されると指定されたreplica数分、APIサーバーの[`POST /api/v1/namespaces/{namespace}/pods`](http://kubernetes.io/docs/api-reference/v1/operations/#_create_a_pod)を使ってpodが生成されます。この時点ではあくまでリソースとしてのPodができるだけで、どのノードにも割り当てられていません。

参考: アクティブなPodの条件

```go:pkg/controller/controller_utils.go
func IsPodActive(p api.Pod) bool {
	return api.PodSucceeded != p.Status.Phase &&
		api.PodFailed != p.Status.Phase &&
		p.DeletionTimestamp == nil
}
```

## schedulerの処理

スケジューラーは、APIサーバーの[`GET /api/v1/pods`](http://kubernetes.io/docs/api-reference/v1/operations/#_list_or_watch_objects_of_kind_pod_2)を使ってノードに未割当のPodを監視しています(割り当て済みPodも監視しています)。このAPIには`fieldSelector`というパラメータでPodのfieldに対する条件が付けることができ、これを使って未割当、割り当て済みのPodを指定しています。

未割当のPodは[NodeSelector](http://kubernetes.io/docs/user-guide/node-selection/)などの条件と優先度を見てNodeが選択され、APIサーバーの[`POST /api/v1/namespaces/{namespace}/bindings`](http://kubernetes.io/docs/api-reference/v1/operations/#_create_a_binding)を使ってNodeの割り当て(Binding)を行います。
(参考: [plugin/pkg/scheduler/generic_scheduler.go](https://github.com/kubernetes/kubernetes/blob/f3f6ffaa283024977a04b921230c224198170e05/plugin/pkg/scheduler/generic_scheduler.go#L70))

参考: 未割当のPodのselect条件

```go:pkg/scheduler/factory/factory.go
// Returns a cache.ListWatch that finds all pods that need to be
// scheduled.
func (factory *ConfigFactory) createUnassignedNonTerminatedPodLW() *cache.ListWatch {
	selector := fields.ParseSelectorOrDie("spec.nodeName==" + "" + ",status.phase!=" + string(api.PodSucceeded) + ",status.phase!=" + string(api.PodFailed))
	return cache.NewListWatchFromClient(factory.Client, "pods", api.NamespaceAll, selector)
}
```

参考: POSTするBindingの例

```json
{
  "kind": "Binding",
  "apiVersion": "v1",
  "metadata": {
    "name": "frontend-yai9k",
    "namespace": "default",
    "creationTimestamp": null
  },
  "target": {
    "kind": "Node",
    "name": "172.17.4.99"
  }
}
```


## Kubelet

kubeletは、APIサーバーの[`GET /api/v1/pods`](http://kubernetes.io/docs/api-reference/v1/operations/#_list_or_watch_objects_of_kind_pod_2)を使って自Nodeに割り当てられたPod(`fieldSelector`パラメータNode名を指定)を監視(watch)しています。そしてPodに定義されたコンテナをdocker(rktも選択可)起動します。Pending, RunningなどのPodのステータスは[`PUT /api/v1/namespaces/{namespace}/pods/{name}/status`](http://kubernetes.io/docs/api-reference/v1/operations/#_replace_status_of_the_specified_pod)を通して更新します。

また、Node自身の情報を[`PUT /api/v1/nodes/{name}/status`](http://kubernetes.io/docs/api-reference/v1/operations/#_replace_status_of_the_specified_node)を使って定期的(デフォルト10秒。kubeletの`--node-status-update-frequency`オプションで指定可)に更新します。

参考: 自分のノードに割り当てられたPodの選択

```go:pkg/kubelet/config/apiserver.go
func NewSourceApiserver(c *clientset.Clientset, nodeName string, updates chan<- interface{}) {
  // spec.nodeNameが自Nodeのものを選択してwatch
	lw := cache.NewListWatchFromClient(c.CoreClient, "pods", api.NamespaceAll, fields.OneTermEqualSelector(api.PodHostField, nodeName))
	newSourceApiserverFromLW(lw, updates)
}
```

## Docker, rkt

kubeletは割り当てられたPodのコンテナを、kubeletの起動オプション`--container-runtime`で指定したコンテナランタイムで起動します(デフォルトはDocker。[rkt](https://github.com/coreos/rkt)も選択可)。ここでやっと実際のコンテナが起動します:tada:。Dockerの場合ネットワークネームスペースを保持共有するための仕組みとして、[Pauseコンテナ](http://qiita.com/tkusumi/items/c2a92cd52bfdb9edd613#pause%E3%82%B3%E3%83%B3%E3%83%86%E3%83%8A)も一緒に起動します。


## 参考: Kubernetesコンポーネントのログ

Kubernetesのコンポーネントは共通して[glog](https://godoc.org/github.com/golang/glog)というログライブラリが使われています(参考: [master/docs/devel/logging.md](https://github.com/kubernetes/kubernetes/blob/master/docs/devel/logging.md))。どのコンポーネントも`--v`オプションでログレベルが指定でき、内部の動作を確認するのに便利です。コード中で使われているの最大のログレベルは**10**でした