kubespray (kargo-cli) で20分で Kubernetes クラスタを作る

# はじめに

`kubespray` を使うと簡単に Kubernetes のクラスタを構築できます。
AWS や GCE にも対応しています。
本家サイトはこちら https://docs.kubespray.io/

ここでは vSphere ESXi で用意した CentOS 7.2 の VM 3台の Kubernetes クラスタを構築します。
この時点での `kubespray` のバージョンは `0.4.5` でした。

| ホスト名 | IPアドレス | 別名 | 役割 |
|:---:|:---:|:---:|:---:|
|kube01.localdomain|192.168.100.1|node1| Master + Node + Etcd |
|kube02.localdomain|192.168.100.2|node2| Master + Node + Etcd |
|kube03.localdomain|192.168.100.3|node3| Node + Etcd |
|kargo.localdomain|192.168.100.10|N/A| kargo 実行ホスト |

テスト用なのでそれぞれ 1 vCPU, 1GB メモリ しか割り当てていません。

# 準備

`kargo` を実行するホストに必要なものをインストールします。
こちらも CentOS 7.2 です。

```console
# yum install -y ansible git gcc python-pip python-devel libffi-devel openssl-devel
# pip install kargo

# kargo -v
kargo 0.4.5
```

鍵ペアを生成し、VM に公開鍵を配る

```console
# ssh-keygen -b 2048 -N ""
# ssh-copy-id kube01.localdomain
# ssh-copy-id kube02.localdomain
# ssh-copy-id kube03.localdomain
```

# セットアップ

まずは inventory を作るため、prepare コマンドを実行します。
ここでは 3 VM で以下の図の構成になります。

![68747470733a2f2f7333322e706f7374696d672e6f72672f387137676e733875742f336e6f6465732e706e67.png](https://qiita-image-store.s3.amazonaws.com/0/44206/9fda2b04-9136-7e4b-6772-f148e406d698.png)
( [kubespray/kargo-cli から引用](https://github.com/kubespray/kargo-cli) )

- M ... Master
- N ... Node
- E ... etcd



```console
# kargo prepare --nodes \
node1[ansible_ssh_host=kube01.localdomain] \
node2[ansible_ssh_host=kube02.localdomain] \
node3[ansible_ssh_host=kube03.localdomain]

CLONING KARGO GIT REPO *********************************************************
Cloning into '/root/.kargo'...
kargo repo cloned

WRITTING INVENTORY *************************************************************
Inventory generated : /root/.kargo/inventory/inventory.cfg
```

```ini:/root/.kargo/inventory/inventory.cfg
[kube-master]
node1
node2

[all]
node1           ansible_ssh_host=kube01.localdomain
node2           ansible_ssh_host=kube02.localdomain
node3           ansible_ssh_host=kube03.localdomain

[k8s-cluster:children]
kube-node
kube-master

[kube-node]
node1
node2
node3

[etcd]
node1
node2
node3
```


[Versions](https://docs.kubespray.io/#versions) を見ると Kubernetes は `1.3.0` との記載がありますが、
この後に API Server の認可のテストをやる予定なので、Kubernetes のバージョンを `1.3.4` に変更してセットアップします。
playbook の中は読まずに grep であたりをつけて書き換えます。
(勝手に変更しているので、動作保証はありません)

```yaml:/root/.kargo/roles/download/vars/kube_versions.yml
kube_version: v1.3.4
```

```yaml:/root/.kargo/roles/uploads/vars/kube_versions.yml
kube_version: v1.3.4
```

prepare で inventory が作成されたので、deploy します。
ついでに `time` コマンドで時間を測ってみます。

```console
# time kargo deploy --verbose -u root -k .ssh/id_rsa -n flannel
Identity added: /root/.ssh/id_rsa (/root/.ssh/id_rsa)

CHECKING SSH CONNECTIONS *******************************************************
node1 | SUCCESS => {
    "changed": false,
    "ping": "pong"
}
node2 | SUCCESS => {
    "changed": false,
    "ping": "pong"
}
node3 | SUCCESS => {
    "changed": false,
    "ping": "pong"
}
All hosts are reachable
/usr/bin/ansible-playbook --ssh-extra-args -o StrictHostKeyChecking=no -u root -b --become-user=root -i /root/.kargo/inventory/inventory.cfg /root/.kargo/cluster.yml -e kube_network_plugin=flannel -vvvv
Run kubernetes cluster deployment with the above command ? [Y/n] y

(省略)

PLAY RECAP *********************************************************************
node1                      : ok=406  changed=85   unreachable=0    failed=0
node2                      : ok=384  changed=75   unreachable=0    failed=0
node3                      : ok=276  changed=66   unreachable=0    failed=0

Kubernetes deployed successfuly

real    16m0.400s
user    4m36.447s
sys     1m33.895s
```

成功しました。16分ほどかかりました。
(実は最初 network plugin を `calico` にして試したのですが、妙な問題が起きたので変更しました)


# クラスタの確認

バージョンが `1.3.4` になっているか確認します。

```console
# ssh kube01.localdomain kubectl version
Client Version: version.Info{Major:"1", Minor:"3", GitVersion:"v1.3.4+coreos.0", GitCommit:"be9bf3e842a90537e48361aded2872e389e902e7", GitTreeState:"clean", BuildDate:"2016-08-02T00:54:53Z", GoVersion:"go1.6.2", Compiler:"gc", Platform:"linux/amd64"}
Server Version: version.Info{Major:"1", Minor:"3", GitVersion:"v1.3.4+coreos.0", GitCommit:"be9bf3e842a90537e48361aded2872e389e902e7", GitTreeState:"clean", BuildDate:"2016-08-02T00:54:53Z", GoVersion:"go1.6.2", Compiler:"gc", Platform:"linux/amd64"}
```

バージョンは良さそうです。どんな環境になったのか見てみます。
以降 `node1` に相当する `kube01` で作業します。

```console
# kubectl get node
NAME      STATUS    AGE
node1     Ready     3m
node2     Ready     3m
node3     Ready     3m
```

3台とも Node として認識されています。

```console
# etcdctl cluster-health
member 54d50575943734fe is healthy: got healthy result from http://192.168.100.1:2379
member a1b38d7fbca44dce is healthy: got healthy result from http://192.168.100.2:2379
member b11d7f87d0d5249d is healthy: got healthy result from http://192.168.100.3:2379
cluster is healthy
```

etcd は 3台でクラスタになっています。
認証なしで外部から etcd にアクセス可能な状態になっているようなので、本番運用する場合は不足がありますが、テストで使う分には良さそうです。

```console
# kubectl cluster-info
Kubernetes master is running at http://localhost:8080
dnsmasq is running at http://localhost:8080/api/v1/proxy/namespaces/kube-system/services/dnsmasq
kubedns is running at http://localhost:8080/api/v1/proxy/namespaces/kube-system/services/kubedns

To further debug and diagnose cluster problems, use 'kubectl cluster-info dump'.

# kubectl get svc --all-namespaces
NAMESPACE     NAME         CLUSTER-IP   EXTERNAL-IP   PORT(S)         AGE
default       kubernetes   10.233.0.1   <none>        443/TCP         4m
kube-system   dnsmasq      10.233.0.2   <none>        53/TCP,53/UDP   3m
kube-system   kubedns      10.233.0.3   <none>        53/UDP,53/TCP   2m
```

クラスタサービスとして API Server 以外にも dnsmasq, kubedns が動いています。

```console
# kubectl get pod --all-namespaces -o wide
NAMESPACE     NAME                            READY     STATUS    RESTARTS   AGE       IP              NODE
kube-system   dnsmasq-1wt2x                   1/1       Running   0          4m        10.233.71.2     node3
kube-system   dnsmasq-fazxk                   1/1       Running   0          4m        10.233.96.2     node1
kube-system   dnsmasq-myvjm                   1/1       Running   0          4m        10.233.74.2     node2
kube-system   flannel-node1                   2/2       Running   2          4m        192.168.100.1   node1
kube-system   flannel-node2                   2/2       Running   2          4m        192.168.100.2   node2
kube-system   flannel-node3                   2/2       Running   2          4m        192.168.100.3   node3
kube-system   kube-apiserver-node1            1/1       Running   0          4m        192.168.100.1   node1
kube-system   kube-apiserver-node2            1/1       Running   0          4m        192.168.100.2   node2
kube-system   kube-controller-manager-node1   1/1       Running   0          4m        192.168.100.1   node1
kube-system   kube-controller-manager-node2   1/1       Running   0          4m        192.168.100.2   node2
kube-system   kube-proxy-node1                1/1       Running   1          4m        192.168.100.1   node1
kube-system   kube-proxy-node2                1/1       Running   1          4m        192.168.100.2   node2
kube-system   kube-proxy-node3                1/1       Running   1          3m        192.168.100.3   node3
kube-system   kube-scheduler-node1            1/1       Running   0          4m        192.168.100.1   node1
kube-system   kube-scheduler-node2            1/1       Running   0          4m        192.168.100.2   node2
kube-system   kubedns-6mhhn                   4/4       Running   0          2m        10.233.71.3     node3
```

Kubernetes のコンポーネントも Pod としてデプロイされているのが分かります。
`node3` は API Server などが動いておらず `Master` の機能を持っていません。

API Server について、もう少し詳しく見てみます。

```console
# ps -eo args | grep hyperkube\ apiserve[r] | sed "s/ /\n/g"
/hyperkube
apiserver
--advertise-address=192.168.100.1
--etcd-servers=http://127.0.0.1:2379
--insecure-bind-address=127.0.0.1
--apiserver-count=2
--admission-control=NamespaceLifecycle,NamespaceExists,LimitRanger,SecurityContextDeny,ServiceAccount,ResourceQuota
--service-cluster-ip-range=10.233.0.0/18
--client-ca-file=/etc/kubernetes/ssl/ca.pem
--basic-auth-file=/etc/kubernetes/users/known_users.csv
--tls-cert-file=/etc/kubernetes/ssl/apiserver.pem
--tls-private-key-file=/etc/kubernetes/ssl/apiserver-key.pem
--token-auth-file=/etc/kubernetes/tokens/known_tokens.csv
--service-account-key-file=/etc/kubernetes/ssl/apiserver-key.pem
--secure-port=443
--insecure-port=8080
--v=2
--allow-privileged=true
2>&1
>>
/var/log/kubernetes/kube-apiserver.log

# cat /etc/kubernetes/users/known_users.csv
changeme,kube,admin
changeme,root,admin
# cat /etc/kubernetes/tokens/known_tokens.csv
lrlochu6hHuzadjegzU4QBaqqxXaswIa,system:kubectl-node1,system:kubectl-node1
4Y9RHeYN6ouIRiFfjucDE6wmDgIJ5Dcn,system:kubectl-node2,system:kubectl-node2
F3FA1y5WKj0tLI7kiw8TJxNe96uMI2tw,system:kubelet-node1,system:kubelet-node1
Ftj02y8Nld2smdiEPEFzTHqCOZ6pUDJO,system:kubelet-node2,system:kubelet-node2
f44IGsNvLVzu1dL7OM2rBfTuGpL8iZjc,system:kubelet-node3,system:kubelet-node3
```

`known_users.csv` は Basic 認証で使われるものですが、本番運用する場合はこれを変更する必要があるでしょう。
token は `.kargo/roles/kubernetes/secrets/files/kube-gen-token.sh` で /dev/urandom を使って生成されているようです。



続いて DNS の機能も見てみます。

```console:dnsmasqについて確認
# kubectl --namespace=kube-system get pod -l k8s-app=dnsmasq
NAME            READY     STATUS    RESTARTS   AGE
dnsmasq-1wt2x   1/1       Running   0          5m
dnsmasq-fazxk   1/1       Running   0          5m
dnsmasq-myvjm   1/1       Running   0          5m
# kubectl --namespace=kube-system exec -ti dnsmasq-1wt2x /bin/sh
# cat /etc/dnsmasq.d/01-kube-dns.conf
#Listen on localhost
bind-interfaces
listen-address=0.0.0.0

addn-hosts=/etc/hosts

bogus-priv

#Set upstream dns servers
server=8.8.8.8
server=8.8.4.4

# Forward k8s domain to kube-dns
server=/cluster.local/10.233.0.3

# exit
# kubectl --namespace=kube-system get svc -l k8s-app=dnsmasq
NAME      CLUSTER-IP   EXTERNAL-IP   PORT(S)         AGE
dnsmasq   10.233.0.2   <none>        53/TCP,53/UDP   6m
# kubectl --namespace=kube-system get ep -l k8s-app=dnsmasq
NAME      ENDPOINTS                                                  AGE
dnsmasq   10.233.71.2:53,10.233.74.2:53,10.233.96.2:53 + 3 more...   6m
```

```console:kubednsについて確認
# kubectl --namespace=kube-system get pod -l k8s-app=kubedns
NAME            READY     STATUS    RESTARTS   AGE
kubedns-6mhhn   4/4       Running   0          5m
# kubectl --namespace=kube-system get svc -l k8s-app=kubedns
NAME      CLUSTER-IP   EXTERNAL-IP   PORT(S)         AGE
kubedns   10.233.0.3   <none>        53/UDP,53/TCP   6m
# kubectl --namespace=kube-system get ep -l k8s-app=kubedns
NAME      ENDPOINTS                       AGE
kubedns   10.233.71.3:53,10.233.71.3:53   6m
```

`10.233.0.2` は `dnsmasq` の ClusterIP です。
`kubedns` がレコードを持っている `.cluster.local` も `10.233.0.2` に問い合わせて応答が得られます。
`dnsmasq` の方を確認すると分かりますが、`.cluster.local` のみを `kubedns` に転送し、それ以外は `8.8.8.8` と `8.8.4.4` に転送しています。

ただ、ホストからの名前解決にはちょっと問題がありました。
まずは VM に元々設定していた nameserver がある場合は、共存に問題があるので 3台とも コメントアウトしておきます。

```console
## kube01 のホスト上で実行
# cat /etc/resolv.conf
search default.svc.cluster.local svc.cluster.local cluster.local
nameserver 10.233.0.2
# Generated by NetworkManager
#nameserver 192.168.37.10    ## kargo deploy 前に設定されていた値
options timeout:2
options attempts:2
```

さらに、Reverse Path Filter が有効になっていると受信した NIC と経路情報との不一致が起きたと見なしてパケットが捨てられてしまいます。
(CentOS 7.2 ではデフォルトで有効)
ここでは単に Reverse Path Filter を無効化して対応します。

```console
# cp -ip /usr/lib/sysctl.d/50-default.conf /tmp
# vi /usr/lib/sysctl.d/50-default.conf
```

```diff
--- /tmp/50-default.conf        2015-11-20 13:49:03.000000000 +0900
+++ /usr/lib/sysctl.d/50-default.conf   2016-08-11 01:26:51.845307320 +0900
@@ -21,8 +21,8 @@
 kernel.core_uses_pid = 1

 # Source route verification
-net.ipv4.conf.default.rp_filter = 1
-net.ipv4.conf.all.rp_filter = 1
+net.ipv4.conf.default.rp_filter = 0
+net.ipv4.conf.all.rp_filter = 0

 # Do not accept source routing
 net.ipv4.conf.default.accept_source_route = 0
```

```console
# sysctl -a | grep '\.rp_filter'
net.ipv4.conf.all.rp_filter = 1
net.ipv4.conf.default.rp_filter = 1
net.ipv4.conf.docker0.rp_filter = 1
net.ipv4.conf.eno16777984.rp_filter = 1
net.ipv4.conf.flannel/1.rp_filter = 1
net.ipv4.conf.lo.rp_filter = 0
net.ipv4.conf.veth5039f11.rp_filter = 1
# systemctl restart systemd-sysctl
# sysctl -a | grep '\.rp_filter'
net.ipv4.conf.all.rp_filter = 0
net.ipv4.conf.default.rp_filter = 0
net.ipv4.conf.docker0.rp_filter = 1
net.ipv4.conf.eno16777984.rp_filter = 1
net.ipv4.conf.flannel/1.rp_filter = 1
net.ipv4.conf.lo.rp_filter = 0
net.ipv4.conf.veth5039f11.rp_filter = 1
## NIC が初期化された時点では default=1 だったので全て書き換え
# sysctl -a | grep '\.rp_filter' | while read line; do set -- $line; sysctl -w ${1}=0; done
# sysctl -a | grep '\.rp_filter'
net.ipv4.conf.all.rp_filter = 0
net.ipv4.conf.default.rp_filter = 0
net.ipv4.conf.docker0.rp_filter = 0
net.ipv4.conf.eno16777984.rp_filter = 0
net.ipv4.conf.flannel/1.rp_filter = 0
net.ipv4.conf.lo.rp_filter = 0
net.ipv4.conf.veth5039f11.rp_filter = 0
```

これでコンテナだけでなく、ホストからも名前解決できるようになりました。

```console:
# yum install -y bind-utils
# dig +short kubernetes.default.svc.cluster.local.
10.233.0.1
# dig +short kubernetes.io.
192.30.252.153
192.30.252.154
```


試しに適当な Pod も作って動かしてみます。

```console
# kubectl run test --image=busybox --restart=Never --command -- /bin/sh -c 'while :; do sleep 100; done'
# kubectl get pod -o wide
NAME      READY     STATUS    RESTARTS   AGE       IP            NODE
test      1/1       Running   0          14s       10.233.96.3   node1
# kubectl exec -ti test /bin/sh
# uname -n
test
# ps
PID   USER     TIME   COMMAND
    1 root       0:00 /bin/sh -c while :; do sleep 100; done
    5 root       0:00 sleep 100
    6 root       0:00 /bin/sh
   10 root       0:00 ps
# ip a
1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue
    link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
    inet 127.0.0.1/8 scope host lo
       valid_lft forever preferred_lft forever
    inet6 ::1/128 scope host
       valid_lft forever preferred_lft forever
10: eth0@if11: <BROADCAST,MULTICAST,UP,LOWER_UP,M-DOWN> mtu 1450 qdisc noqueue
    link/ether 02:42:0a:e9:60:03 brd ff:ff:ff:ff:ff:ff
    inet 10.233.96.3/24 scope global eth0
       valid_lft forever preferred_lft forever
    inet6 fe80::42:aff:fee9:6003/64 scope link tentative flags 08
       valid_lft forever preferred_lft forever
# ip r
default via 10.233.96.1 dev eth0
10.233.96.0/24 dev eth0  src 10.233.96.3

# cat /etc/resolv.conf
search default.svc.cluster.local svc.cluster.local cluster.local default.svc.cluster.local svc.cluster.local cluster.local
nameserver 10.233.0.2
options attempts:2
options ndots:5
# nslookup kubernetes.io
Server:    10.233.0.2
Address 1: 10.233.0.2

Name:      kubernetes.io
Address 1: 192.30.252.153 pages.github.com
Address 2: 192.30.252.154 pages.github.com
# nslookup kubernetes.default.svc.cluster.local
Server:    10.233.0.2
Address 1: 10.233.0.2

Name:      kubernetes.default.svc.cluster.local
Address 1: 10.233.0.1

# exit
```

ちゃんと動いているようですね。

ところで・・・
![68747470733a2f2f7333322e706f7374696d672e6f72672f387137676e733875742f336e6f6465732e706e67.png](https://qiita-image-store.s3.amazonaws.com/0/44206/9fda2b04-9136-7e4b-6772-f148e406d698.png)
3台目の VM では Master の機能を持たせていませんでした。
3台目の VM はどちらの VM にいる Master を参照しているのでしょうか。

```console:
## node3に相当するkube03にてコマンド実行
# ps -eo args | grep ^./hyperkube\ kubelet | sed "s/ /\n/g"
./hyperkube
kubelet
--v=2
--api_servers=https://192.168.100.1:443
--address=0.0.0.0
--hostname-override=node3
--allow-privileged=true
--cluster_dns=10.233.0.2
--cluster_domain=cluster.local
--kubeconfig=/etc/kubernetes/node-kubeconfig.yaml
--config=/etc/kubernetes/manifests
--resolv-conf=/etc/resolv.conf

## node1に相当するkube01にてコマンド実行
# ps -eo args | grep ^./hyperkube\ kubelet | sed "s/ /\n/g"
./hyperkube
kubelet
--v=2
--api_servers=http://127.0.0.1:8080
--address=0.0.0.0
--hostname-override=node1
--allow-privileged=true
--cluster_dns=10.233.0.2
--cluster_domain=cluster.local
--kubeconfig=/etc/kubernetes/node-kubeconfig.yaml
--config=/etc/kubernetes/manifests
--resolv-conf=/etc/resolv.conf

## node2に相当するkube02にてコマンド実行
# ps -eo args | grep ^./hyperkube\ kubelet | sed "s/ /\n/g"
./hyperkube
kubelet
--v=2
--api_servers=http://127.0.0.1:8080
--address=0.0.0.0
--hostname-override=node2
--allow-privileged=true
--cluster_dns=10.233.0.2
--cluster_domain=cluster.local
--kubeconfig=/etc/kubernetes/node-kubeconfig.yaml
--config=/etc/kubernetes/manifests
--resolv-conf=/etc/resolv.conf
```

API Server の参照先は  `192.168.100.1` になっています。
この場合 1台目がダウンした場合に 3台目の kubelet も Not Ready 状態になるでしょう。
ここでは触れませんが、本番運用する時には対応が必要でしょう。
他にも 3台目から 1台目の API Server を参照している設定があるので、そちらも注意が必要です。

```console:kube03にてコマンド実行
root       9817  0.0  0.1 115240  1480 ?        Ss   01:14   0:00 /bin/bash /usr/local/bin/kubelet --v=2 --api_servers=https://192.168.100.1:443 --address=0.0.0.0 --hostname-override=node3 --allow-privileged=true --cluster_dns=10.233.0.2 --cluster_domain=cluster.local --kubeconfig=/etc/kubernetes/node-kubeconfig.yaml --config=/etc/kubernetes/manifests --resolv-conf=/etc/resolv.conf
root       9819  0.0  0.9 119152  9076 ?        Sl   01:14   0:00 /usr/bin/docker run --privileged --rm --net=host --pid=host --name=kubelet -v /etc/cni:/etc/cni:ro -v /opt/cni:/opt/cni:ro -v /etc/kubernetes:/etc/kubernetes -v /sys:/sys -v /dev:/dev -v /var/lib/docker:/var/lib/docker -v /var/run:/var/run -v /var/lib/kubelet:/var/lib/kubelet quay.io/coreos/hyperkube:v1.3.4_coreos.0 nsenter --target=1 --mount --wd=. -- ./hyperkube kubelet --v=2 --api_servers=https://192.168.100.1:443 --address=0.0.0.0 --hostname-override=node3 --allow-privileged=true --cluster_dns=10.233.0.2 --cluster_domain=cluster.local --kubeconfig=/etc/kubernetes/node-kubeconfig.yaml --config=/etc/kubernetes/manifests --resolv-conf=/etc/resolv.conf
root       9866  1.1  4.6 464388 46192 ?        Ssl  01:14   0:13 ./hyperkube kubelet --v=2 --api_servers=https://192.168.100.1:443 --address=0.0.0.0 --hostname-override=node3 --allow-privileged=true --cluster_dns=10.233.0.2 --cluster_domain=cluster.local --kubeconfig=/etc/kubernetes/node-kubeconfig.yaml --config=/etc/kubernetes/manifests --resolv-conf=/etc/resolv.conf
root      10250  0.9  2.7 363300 27500 ?        Ssl  01:15   0:11 /hyperkube proxy --v=2 --master=https://192.168.100.1:443 --kubeconfig=/etc/kubernetes/node-kubeconfig.yaml --bind-address=192.168.100.3 --proxy-mode=iptables
```