一見普通なのにKubernetesを破壊するyaml

# まずはこちらをご覧ください

<script id="asciicast-286191" src="https://asciinema.org/a/286191.js" async></script>

## お分かりいただけただろうか？

* `instance-1`がmaster, `instance-2`がnodeのk8sクラスタ上で
* `kubectl create -f`でyamlを投げた
  * yamlの中身は**`instance-2`のIPを`externalIP`に持つService**

```yaml:投げたdestroy.yaml
apiVersion: v1
kind: Service
metadata:
  name: destroy
spec:
  ports:
  - port: 6666
  externalIPs:
  - 10.128.0.9 # ip of instance-2
```

* すると**`instance-2`が落ちた**
* **`instance-2`にSSHするとなぜか`instance-1`にログインした**

一見普通のyamlなのになぜ？

# 解説
## この現象は次の要素を組み合わせると起きます
* kube-proxy mode に IPVS modeを指定したKubernetesクラスタ
* externalIPにノードのIPを指定したサービスを作る

kube-proxy mode, IPVS mode, externalIPについて説明してから、
なぜその組み合わせがノード障害を起こすのか見ていきます

## kube-proxy modeとは？
* Kubernetesdでは、Serviceをトラフィックの受け口として複数Podへのネットワークの振り分けが実現できます
  * クライアントはServiceのIPを介することで、Podにアクセスできます 
* この振り分けの実現方法を指定するのが kube-proxy mode です。
  * デフォルトではiptablesを使うiptables proxy modeです
  * IPVSというLinux Kernelのロードバランサ機能を用いるのがIPVS proxy mode です
      * v1.11でGAになりました  

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/304012/484e38fd-701e-9f14-215a-c8b550e331d3.png)


## IPVS modeの挙動

IPVSは次の２つのコンポーネントを組み合わせてロードバランスを実現します

  * Virtual Server(IPVSサーバー): トラフィックの受け口
  * Real Server: トラフィックの転送先

IPVSでServiceのトラフィックの振り分けを実装することを考えると、
Real ServerにはPodを指定すればよいですが、IPVSサーバーに相当するものがありません。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/304012/63bb6c16-a9b5-e615-86a5-a618565d320d.png)

そこで、KubernetesではノードをIPVSサーバーにしてしまいます。`kube-ipvs0`というダミーのインターフェース(NIC)を作り、
ServiceのIPはそのインターフェースに割り当てます

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/304012/9287d3c8-979e-6b81-f5e6-e6d4ab20de08.png)

## ExternalIPとは
* デフォルトではServiceにはKubernetes内部でのみ使用可能なIPアドレスであるClusterIPだけが自動で割り当てられます
  * Kubernetes外部からはPodにアクセスできません
* 外部からアクセス可能なIPをサービスに紐づけるのがExternalIPです
  * nodeのIPを指定することで、nodeのIPを介してPodへアクセスできるようになります

以上でやっと、冒頭の障害はなぜ起きるのかを説明できる準備ができました

## 冒頭の障害はなぜ起きるのか
* destroy.ymlを投げると、クラスタは次の状態になっています
  * 作成するサービスのExternalIPに`instance-2`のIPを指定しています
  * ipvsモードでは、サービスのipをノードの`kube-ipvs0`インターフェースに割り当てます
  * そのため、`instance-1`の`kube-ipvs0`がExternalIPである`instance-2`のIPを持ってしまいます
      *  `instance-1` → `instance-2`の通信はすべて`kube-ipvs0`に吸収され、`instance-1`に戻ってしまいます
         * 通信不能で`instance-2`が`down`とKubernetesが判断
         * `instance-2`にsshすると`instance-1`にログイン
  
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/304012/69cdf7b6-6cbd-512a-e40a-56d65d4db5a0.png)


# 補足
## 自分のクラスタのkube-proxy modeの設定を確認する方法

* masterで次を実行すると確認できます
 
```bash
kubectl get configmap kube-proxy -n kube-system -o yaml | grep 'mode:'
```


## IPVSモードは必要？
大規模環境では有効です

* iptablesモードは次の理由から、大規模なクラスタでは重くなってしまいます
  * Podやサービスが増えるほどにiptablesのルールが大量に増える
  * パケットの転送はiptablesのテーブルを辿りながらマッチするルールを探索することになり、ルールが多いほど転送も重くなる(シーケンシャルリスト)
* そのためIPVSモードが導入されました。
  * IPVSはハッシュテーブルのため、iptablesのような負荷は起きません
 
# 参考
* [GithubのIssue](https://github.com/kubernetes/kubernetes/issues/75290)
* [kube-proxyの公式の解説](https://kubernetes.io/ja/docs/concepts/services-networking/service/#virtual-ips-and-service-proxies)
* [IPVSモードのk8sブログ](https://kubernetes.io/blog/2018/07/09/ipvs-based-in-cluster-load-balancing-deep-dive/)