Kubernetesに触れる: Kubernetes内のネットワークとDNS

[Kubernetesに触れる](https://qiita.com/kenji-kondo/items/a5d497007f1b2099d29b) の続き。KubernetesのNetwork関連の話題について。

# Networkについて

## Pod間の通信

Kubernetesクラスタをkubeadmなどを使って構築しようとすると、以下のようなコマンドを実行することになる。

```
kubectl apply -n kube-system -f \
    "https://cloud.weave.works/k8s/net?k8s-version=$(kubectl version | base64 |tr -d '\n')"
```

この作業で、Kubernetes上でプライベートネットワークが作られる。Weave Netはデフォルトでは`10.32.0.0/12`というセグメントでネットワークを作る。このセグメントの指定は変更可能である。ホストネットワークと被ってはいけないという制約があるので、ホストネットワークが例えば`10.0.0.0/8`である場合は、明示的に別のセグメントを指定する必要がある。

今回はWeave Netを適用したが、他にも選択しがあってFlannelやCalicoなどがある。[kubeadm](https://kubernetes.io/docs/setup/independent/create-cluster-kubeadm/)のサイト参照。


Podを2つデプロイしてみる。以下はcentos7のContainerイメージを動かして`sleep 3600`を実行したPodがデプロイされている様子。

```shell-session
[node1 ~]$ kubectl get po -o wide
NAME      READY     STATUS    RESTARTS   AGE       IP          NODE      NOMINATED NODE
cent-1    1/1       Running   0          17s       10.44.0.1   node3     <none>
cent-2    1/1       Running   0          13s       10.36.0.1   node2     <none>
```

Podを見てみると、`10.32.0.0/12`の中にあるIPが割り当てられている。このように、Kubernetes上ではPod一つにつき一つのIPが割り当てられる。

色々なところからpingしてみる。今回は、`node1`がMasterで`node2, node3, node4`がNodesである。

Node`node2`からPod`cent-1`へping

```shell-session
[node2 ~]$ ping 10.44.0.1
PING 10.44.0.1 (10.44.0.1) 56(84) bytes of data.
64 bytes from 10.44.0.1: icmp_seq=1 ttl=64 time=1.45 ms
64 bytes from 10.44.0.1: icmp_seq=2 ttl=64 time=0.769 ms
^C
--- 10.44.0.1 ping statistics ---
2 packets transmitted, 2 received, 0% packet loss, time 1000ms
rtt min/avg/max/mdev = 0.769/1.110/1.452/0.343 ms
```

Pod`cent-1`からPod`cent-2`へping

```shell-session
[node1 ~]$ kubectl exec -it cent-1 /bin/sh
sh-4.2# ping 10.36.0.1
PING 10.36.0.1 (10.36.0.1) 56(84) bytes of data.
64 bytes from 10.36.0.1: icmp_seq=1 ttl=64 time=2.54 ms
64 bytes from 10.36.0.1: icmp_seq=2 ttl=64 time=0.781 ms
^C
--- 10.36.0.1 ping statistics ---
2 packets transmitted, 2 received, 0% packet loss, time 1001ms
rtt min/avg/max/mdev = 0.781/1.663/2.545/0.882 ms
```

このように、Kubernetesの中ではクラスタ固有のプライベートネットワークがある。この仕組みによって、Kubernetesクラスタ毎に、ホストのネットワークに依存しない、独立したネットワーク環境が実現できる。

## Pod内部のContainer間の通信

翻って、Pod内部に複数のContainerがある場合を考える。

先に説明したように、Kubernetes上ではPodに一つのIPが割り当てられる。

ではPod内のContainer間の通信はどのように行われるか？

yum install tcpdump net-toolsをする。

```yaml:cent-cent.yml
apiVersion: v1
kind: Pod
metadata:
  name: cent-cent
  labels:
    app: myapp
spec:
  containers:
  - name: cent7-1
    image: centos:7
    command: ['/bin/sh', '-c', '/usr/bin/yum install tcpdump net-tools -y && sleep 3600']
  - name: cent7-2
    image: centos:7
    command: ['/bin/sh', '-c', '/usr/bin/yum install tcpdump net-tools -y && sleep 3600']
```

ifconfigでたしかめると、同じIPがついている。

```shell-session

[node1 ~]$ kubectl exec -it cent-cent -c cent7-1 /usr/sbin/ifconfig
eth0: flags=4163<UP,BROADCAST,RUNNING,MULTICAST>  mtu 65535
        inet 10.47.0.1  netmask 255.240.0.0  broadcast 10.47.255.255
        ether da:aa:b4:3f:6b:87  txqueuelen 0  (Ethernet)
        RX packets 14553  bytes 21243468 (20.2 MiB)
        RX errors 0  dropped 0  overruns 0  frame 0
        TX packets 2958  bytes 210548 (205.6 KiB)
        TX errors 0  dropped 0 overruns 0  carrier 0  collisions 0

lo: flags=73<UP,LOOPBACK,RUNNING>  mtu 65536
        inet 127.0.0.1  netmask 255.0.0.0
        loop  txqueuelen 1  (Local Loopback)
        RX packets 0  bytes 0 (0.0 B)
        RX errors 0  dropped 0  overruns 0  frame 0
        TX packets 0  bytes 0 (0.0 B)
        TX errors 0  dropped 0 overruns 0  carrier 0  collisions 0

[node1 ~]$ kubectl exec -it cent-cent -c cent7-2 /usr/sbin/ifconfig
eth0: flags=4163<UP,BROADCAST,RUNNING,MULTICAST>  mtu 65535
        inet 10.47.0.1  netmask 255.240.0.0  broadcast 10.47.255.255
        ether da:aa:b4:3f:6b:87  txqueuelen 0  (Ethernet)
        RX packets 14553  bytes 21243468 (20.2 MiB)
        RX errors 0  dropped 0  overruns 0  frame 0
        TX packets 2958  bytes 210548 (205.6 KiB)
        TX errors 0  dropped 0 overruns 0  carrier 0  collisions 0

lo: flags=73<UP,LOOPBACK,RUNNING>  mtu 65536
        inet 127.0.0.1  netmask 255.0.0.0
        loop  txqueuelen 1  (Local Loopback)
        RX packets 0  bytes 0 (0.0 B)
        RX errors 0  dropped 0  overruns 0  frame 0
        TX packets 0  bytes 0 (0.0 B)
        TX errors 0  dropped 0 overruns 0  carrier 0  collisions 0
```

ためしに別のノードからpingしてtcpdumpしてみる。

```
[node1 ~]$ kubectl exec -it cent-cent -c cent7-1 /bin/sh
sh-4.2# tcpdump -i eth0 icmp
tcpdump: verbose output suppressed, use -v or -vv for full protocol decode
listening on eth0, link-type EN10MB (Ethernet), capture size 262144 bytes
08:30:37.554918 IP 10.36.0.0 > cent-cent: ICMP echo request, id 38327, seq 39, length 64
08:30:37.554953 IP cent-cent > 10.36.0.0: ICMP echo reply, id 38327, seq 39, length 64
08:30:38.554826 IP 10.36.0.0 > cent-cent: ICMP echo request, id 38327, seq 40, length 64
08:30:38.554899 IP cent-cent > 10.36.0.0: ICMP echo reply, id 38327, seq 40, length 64

[node1 ~]$ kubectl exec -it cent-cent -c cent7-2 /bin/sh
sh-4.2# tcpdump -i eth0 icmp
tcpdump: verbose output suppressed, use -v or -vv for full protocol decode
listening on eth0, link-type EN10MB (Ethernet), capture size 262144 bytes
08:31:03.570873 IP 10.36.0.0 > cent-cent: ICMP echo request, id 38327, seq 65, length 64
08:31:03.570908 IP cent-cent > 10.36.0.0: ICMP echo reply, id 38327, seq 65, length 64
08:31:04.570800 IP 10.36.0.0 > cent-cent: ICMP echo request, id 38327, seq 66, length 64
08:31:04.570836 IP cent-cent > 10.36.0.0: ICMP echo reply, id 38327, seq 66, length 64
```

どちらもきてる。

VMの比喩を引きずってるとわかりにくい現象であるが、ここからもわかるように、podはIPを共有しているため、このPodへpingすると、それぞれのContainerにあたる。したがってこの内のContainer一にアクセスするには、ポートでわけるしかない。実際、Pod内のContainerが同じポートをバインドすることはできない。例えばPodに二つのNginxを立てようとして80番ポートをそれぞれのContainerがバインドしようとした場合、エラーとなってPodは起動できない。


# DNSについて

## いろいろな名前解決の方法

以前にも紹介したが、ClusterIPはこれはロードバランシングの機能を提供してくれる。今回はこれの名前解決について。

KubernetesはDNSも提供してくれる。ClusterIPには名前解決可能な名前がつく。

前回と同じnginxの例で考える。

```shell-session
[node1 ~]$ k get pods
NAME                       READY     STATUS    RESTARTS   AGE
my-nginx-b685dc965-4hc7w   1/1       Running   0          1m
my-nginx-b685dc965-7527g   1/1       Running   0          1m
my-nginx-b685dc965-mkv9l   1/1       Running   0          1m
[node1 ~]$ k get svc
NAME           TYPE        CLUSTER-IP     EXTERNAL-IP   PORT(S)    AGE
kubernetes     ClusterIP   10.96.0.1      <none>        443/TCP    1h
my-nginx-svc   ClusterIP   10.110.220.5   <none>        8080/TCP   1m
```

それぞれのPodは`10.96.0.10`を`nameserver`として指定している。

```
[node1 ~]$ k exec -it my-nginx-b685dc965-4hc7w /bin/sh
# cat /etc/resolv.conf
nameserver 10.96.0.10
search default.svc.cluster.local svc.cluster.local cluster.local
options ndots:5
```

ここに向けて`my-nginx-svc.default.svc.cluster.local`という名前をひくとIPがかえってくる。帰ってきたものは、ClusterIPのIPになっている。したがって、Podからあるクラスターにランダムにアクセスするには(たとえばREST APIを投げるとかの用途では)、自分で定義した`my-nginx-svc`という名前に対してリクエストを投げつければよい。

```
[node1 ~]$ dig my-nginx-svc.default.svc.cluster.local @10.96.0.10

; <<>> DiG 9.9.4-RedHat-9.9.4-73.el7_6 <<>> my-nginx-svc.default.svc.cluster.local @10.96.0.10
;; global options: +cmd
;; Got answer:
;; ->>HEADER<<- opcode: QUERY, status: NOERROR, id: 21647
;; flags: qr rd ra; QUERY: 1, ANSWER: 1, AUTHORITY: 0, ADDITIONAL: 1

;; OPT PSEUDOSECTION:
; EDNS: version: 0, flags:; udp: 4096
;; QUESTION SECTION:
;my-nginx-svc.default.svc.cluster.local.        IN A

;; ANSWER SECTION:
my-nginx-svc.default.svc.cluster.local. 1 IN A  10.110.220.5

;; Query time: 0 msec
;; SERVER: 10.96.0.10#53(10.96.0.10)
;; WHEN: Sat Mar 09 09:05:23 UTC 2019
;; MSG SIZE  rcvd: 121
```

上記の例ではAレコードを返した。これをCNAMEレコードとして扱うこともできる。そのためにはPodでデプロイしていたものを、StatefulSetとしてデプロイしておく必要がある。StatefulSetとは、Kubernetes上でStatefulな役割を持つContainerを扱いたいときに使うリソース。たとえばディスクへのアクセスが必要なDBやKafkaをデプロイしたい場合はこれを利用する。

```yaml:nginx-headless.yaml
---
kind: Service
metadata:
  name: my-nginx-svc
  labels:
    app: nginx
spec:
  type: ClusterIP
  clusterIP: None
  ports:
  - port: 8080
    targetPort: 80
  selector:
    app: nginx
---
apiVersion: apps/v1
kind: StatefulSet
metadata:
  name: my-nginx
  labels:
    app: nginx
spec:
  serviceName: my-nginx-svc
  replicas: 3
  selector:
    matchLabels:
      app: nginx
  template:
    metadata:
      labels:
        app: nginx
    spec:
      containers:
      - name: nginx
        image: nginx:latest
```

これを適用するとServiceとStatefulSetがデプロイされる。まずServiceにはClusterIPがついていない。一方、それぞれのPodの名前が`-0`、`-1`、... といった、連番になっている。

```shell-session
[node1 ~]$ kubectl get pods
NAME         READY     STATUS    RESTARTS   AGE
my-nginx-0   1/1       Running   0          2m
my-nginx-1   1/1       Running   0          1m
my-nginx-2   1/1       Running   0          1m
[node1 ~]$ kubectl get svc
NAME           TYPE        CLUSTER-IP   EXTERNAL-IP   PORT(S)    AGE
kubernetes     ClusterIP   10.96.0.1    <none>        443/TCP    1h
my-nginx-svc   ClusterIP   None         <none>        8080/TCP   2m
```

ここで`my-nginx-svc`の名前を引いてみるとCNAMEでかえってくることがわかる。また、この状態ならばそれぞれのPodに対しても名前解決して通信をすることができる。

```shell-session
[node1 ~]$ dig my-nginx-svc.default.svc.cluster.local @10.96.0.10

; <<>> DiG 9.9.4-RedHat-9.9.4-73.el7_6 <<>> my-nginx-svc.default.svc.cluster.local @10.96.0.10
;; global options: +cmd
;; Got answer:
;; ->>HEADER<<- opcode: QUERY, status: NOERROR, id: 9330
;; flags: qr aa rd ra; QUERY: 1, ANSWER: 3, AUTHORITY: 0, ADDITIONAL: 1

;; OPT PSEUDOSECTION:
; EDNS: version: 0, flags:; udp: 4096
;; QUESTION SECTION:
;my-nginx-svc.default.svc.cluster.local.        IN A

;; ANSWER SECTION:
my-nginx-svc.default.svc.cluster.local. 5 IN A  10.36.0.1
my-nginx-svc.default.svc.cluster.local. 5 IN A  10.44.0.1
my-nginx-svc.default.svc.cluster.local. 5 IN A  10.47.0.1

;; Query time: 1 msec
;; SERVER: 10.96.0.10#53(10.96.0.10)
;; WHEN: Sat Mar 09 09:19:23 UTC 2019
;; MSG SIZE  rcvd: 229

[node1 ~]$ dig my-nginx-0.my-nginx-svc.default.svc.cluster.local @10.96.0.10

; <<>> DiG 9.9.4-RedHat-9.9.4-73.el7_6 <<>> my-nginx-0.my-nginx-svc.default.svc.cluster.local @10.96.0.10
;; global options: +cmd
;; Got answer:
;; ->>HEADER<<- opcode: QUERY, status: NOERROR, id: 8834
;; flags: qr aa rd ra; QUERY: 1, ANSWER: 1, AUTHORITY: 0, ADDITIONAL: 1

;; OPT PSEUDOSECTION:
; EDNS: version: 0, flags:; udp: 4096
;; QUESTION SECTION:
;my-nginx-0.my-nginx-svc.default.svc.cluster.local. IN A

;; ANSWER SECTION:
my-nginx-0.my-nginx-svc.default.svc.cluster.local. 5 IN A 10.44.0.1

;; Query time: 0 msec
;; SERVER: 10.96.0.10#53(10.96.0.10)
;; WHEN: Sat Mar 09 09:21:35 UTC 2019
;; MSG SIZE  rcvd: 143
```

DNSについての最後に、Kubernetesクラスタ内から外部への通信の際の名前解決について。

以下のようなServiceを作れば、外部のリソースの名前もラップできる。

```yaml:google-external.yaml
kind: Service
apiVersion: v1
metadata:
  name: google-externalname
  namespace: default
spec:
  type: ExternalName
  externalName: google.com
```

名前をひくと、CNAMEでgoogle.comがかえってきた。

```
[node1 ~]$ kubectl get svc
NAME                  TYPE           CLUSTER-IP   EXTERNAL-IP   PORT(S)    AGE
google-externalname   ExternalName   <none>       google.com    <none>     5s
kubernetes            ClusterIP      10.96.0.1    <none>        443/TCP    2h
my-nginx-svc          ClusterIP      None         <none>        8080/TCP   16m
[node1 ~]$ dig google-externalname.default.svc.cluster.local @10.96.0.10

; <<>> DiG 9.9.4-RedHat-9.9.4-73.el7_6 <<>> google-externalname.default.svc.cluster.local @10.96.0.10
;; global options: +cmd
;; Got answer:
;; ->>HEADER<<- opcode: QUERY, status: NOERROR, id: 20827
;; flags: qr aa rd ra; QUERY: 1, ANSWER: 2, AUTHORITY: 0, ADDITIONAL: 1

;; OPT PSEUDOSECTION:
; EDNS: version: 0, flags:; udp: 4096
;; QUESTION SECTION:
;google-externalname.default.svc.cluster.local. IN A

;; ANSWER SECTION:
google-externalname.default.svc.cluster.local. 5 IN CNAME google.com.
google.com.             5       IN      A       172.217.168.206

;; Query time: 10 msec
;; SERVER: 10.96.0.10#53(10.96.0.10)
;; WHEN: Sat Mar 09 09:31:56 UTC 2019
;; MSG SIZE  rcvd: 169
```

## 何がうれしいのか

これまでは、IPやエンドポイントの管理を自分たちで行う必要があった。特にProduction環境やStaging環境などでエンドポイントが異なる場合に、アプリケーションのコンフィグファイルにそれぞれのエンドポイントのホストネームを記述して、実行時に適用仕分けたりだとか。

一方、上の仕組みならIPやエンドポイントの名前の管理をする必要がそもそもなくなる。Kubernetesでこの機能が一番感動した。

# 外部からのアクセス

Webサイトを公開するときなど、外部からアクセスする際は、選択しとしてLoadBalancerかIngressがある。あまり詳細はしない。

これらの違いはここに詳しい。

* [Kubernetes NodePort vs LoadBalancer vs Ingress? When should I use what?](https://medium.com/google-cloud/kubernetes-nodeport-vs-loadbalancer-vs-ingress-when-should-i-use-what-922f010849e0)

それぞれ以下のような特徴がある。

* LoadBalancer
  * L4のロードバランシング
* Ingress
  * L7のロードバランシング
  * パスベースでのルーティング
  * SSL/TSLの終端になる。
  * 実装はNginxなど。

# 書こうと思ったけどやめたこと

* NetworkPolicy
* Kafka Cluster(Zookeeperも含む) をStatefulSetでk8s上にデプロイしてみる。

# 参考

* AKSでNetworkPolicyを有効にする: https://docs.microsoft.com/ja-jp/azure/aks/use-network-policies#code-try-0