Vagrant + KVMでHAクラスタのkubernetesを構築してみる

去年まではオンプレミスのOpenShiftで、GitLabやJenkinsなどなどの開発ツールを手軽に動かしていた。kubernetesを生で使うよりは、HA Proxyを使ったルータ機能がセットになっているなど構築や運用が楽だと思っていた。

しかし、kubernetesの進歩は早く、OpenShiftが使うkubernetesのバージョンがちょっと古かったり、ちょっとしたトラブルでOpenShiftのドキュメントを調べて判らなければkubernetesのドキュメントを調べるというのも手間だったりなので、今回は日本語の書籍も充実してきたkubernetesで構築してみることにした。

* 構築手順は主に以下を参考にしました。
    * [書籍][Kubernetes実践ガイド](https://www.amazon.co.jp/Kubernetes%E5%AE%9F%E8%B7%B5%E3%82%AC%E3%82%A4%E3%83%89-%E3%82%AF%E3%83%A9%E3%82%A6%E3%83%89%E3%83%8D%E3%82%A4%E3%83%86%E3%82%A3%E3%83%96%E3%82%A2%E3%83%97%E3%83%AA%E3%82%B1%E3%83%BC%E3%82%B7%E3%83%A7%E3%83%B3%E3%82%92%E6%94%AF%E3%81%88%E3%82%8B%E6%8A%80%E8%A1%93-impress-top-gear/dp/4295006637/ref=sr_1_1?adgrpid=66487072010&gclid=CjwKCAjw1_PqBRBIEiwA71rmtVyrxTaEepiZSqP84kCXvnijt4tuYWVwQ6BpCuKgS1MullBsb8kYyxoCMs8QAvD_BwE&hvadid=353068023593&hvdev=c&hvlocphy=1009318&hvnetw=g&hvpos=1t1&hvqmt=e&hvrand=4607290721896784831&hvtargid=aud-759377471893%3Akwd-784226074264&hydadcr=27268_11561171&jp-ad-ap=0&keywords=kubernetes%E5%AE%9F%E8%B7%B5%E3%82%AC%E3%82%A4%E3%83%89&qid=1566439122&s=gateway&sr=8-1)
    * [kubeadmドキュメント](https://kubernetes.io/ja/docs/home/)
        * [kubeadmのインストール](https://kubernetes.io/ja/docs/setup/independent/install-kubeadm/)
        * [kubeadmを使用した高可用性クラスターの作成](https://kubernetes.io/ja/docs/setup/independent/high-availability/)
* なお、以下は、HAクラスタとして起動してhello worldアプリケーションの動作を確認したレベルの構築手順です。まだOpenShiftのルータ機能相当や、永続化ストレージも用意していないので、今後、大きな落とし穴がある可能性があるので、注意してください。
    * (2019/8/26追記)ルータや永続化の対応するなかで、まだ勘違いしているかもしませんが、おおまかに以下の見直しを行いました。
        * 仮想サーバにはデフォルトディスクサイズが40Gだったので、ストレージに追加はやめました。
        * vagrant-libvirtが使用する管理ネットワークとか、ingressにMetalLBに割り当てるアドレスプールとか、Heketiは固定IPアドレスしか設定できないとかあって、固定IPを割り当てるようにしました。
        * 永続化にGlusterFSを使用することにし、物理ホスト構築時にディスク領域を50G分空けておきました。


### 今回の到達目標
物理ホスト上に仮想サーバを4台作成し、この4台をマスタノードとワーカーノードとしてKubernetes HAクラスタにします。
![](http://www.plantuml.com/plantuml/png/XP0_IyDG5CNtzodkM8S8VPD3AHKdAQ8WE7LvJHuYVRbFyWS5wP8BOXVfBhW74GTn40G_p1twDNnfOa9YYkivdD-vzqpYH4L96Xg1Dj2mMUMRzJtJ4sUVJ0-3HJhN4Uf4n_l6-ihimMXqf34vFH-FuKu0-3QyjkWKE3mxPdfb-c9QCQsPSgPtpZvS0Y-lyDO5L0p6gc2kZ49VHyBbjwrCpqLVzitqM0A6Cnmi3c8lL76Yes5HExquAH6LjgrVxaR85aBs86GNyLOaivS_LzpOQD6zejB-GyWMGlOWcYjyOzC0flNaC-JCyVBoiIBHU5L8G6jHyBpVUNOwKa0zeHCRPAtBhbu_wwMzXkmQIp7H6AIX-G40)

|ソフト|バージョン|備考|
|---|---|---|
|CentOS|7.6||
|Kubernetes|1.15||
|Docker|18.09|Kubernetes 1.15がサポートしている最新バージョン。|


### 物理ホストの構築
まずCentOS 7.6をVirtualization Hostでインストール。
次に仮想サーバを構築するにあたり、、、

* Virtual Boxは手軽そうだが性能的なオーバヘッドが大きそう。
* VMwareはアカウント作成とかがいろいろと面倒。
ということで、最近はあまり記事を見かけないけど、そこは不具合は枯れている捉えて、性能も良さそうなKVMを選択しました。

で、これ以降の作業でトライ&エラーを繰り返すことが想定されることから、リトライ時間の短縮を図るため、Vagrant + KVMで構築することにしました。

##### VagrantとKVM連携プラグインのインストール

```shell-session
# yum install -y qemu-kvm libvirt libvirt-devel gcc patch
# systemctl start libvirtd
# systemctl enable libvirtd
# yum install -y https://releases.hashicorp.com/vagrant/2.2.5/vagrant_2.2.5_x86_64.rpm
# vagrant plugin install vagrant-libvirt
```

##### 仮想サーバを作成するスクリプト

```ruby:Vagrantfile
Vagrant.configure(2) do |config|
  config.ssh.insert_key = false

  $nodes = [
    { name: "k8s-master1", cpu: "2", memory: "4096", ip: "192.168.122.2"},
    { name: "k8s-master2", cpu: "2", memory: "4096", ip: "192.168.122.3"},
    { name: "k8s-worker1", cpu: "6", memory: "8192", ip: "192.168.122.10"},
    { name: "k8s-worker2", cpu: "6", memory: "8192", ip: "192.168.122.11"}
  ]

  $nodes.each do |param|
    config.vm.define param[:name] do |node|
      node.vm.box = "centos/7"
      node.vm.hostname = param[:name]

      node.vm.provider :libvirt do |v|
        v.cpus = param[:cpu]
        v.memory = param[:memory]
      end

      node.vm.network :private_network, ip: param[:ip], :libvirt__network_name => "default"

      node.vm.provision :shell do |s|
        ssh_insecure_key = File.read("#{Dir.home}/.vagrant.d/insecure_private_key")
        s.inline = <<-SHELL
        mkdir -p /root/.ssh
        echo "#{ssh_insecure_key}" >> /root/.ssh/id_rsa
        chmod 400 /root/.ssh/id_rsa
        cp /home/vagrant/.ssh/authorized_keys /root/.ssh/authorized_keys

        echo "#{ssh_insecure_key}" >> /home/vagrant/.ssh/id_rsa
        chown vagrant /home/vagrant/.ssh/id_rsa
        chmod 400 /home/vagrant/.ssh/id_rsa
        SHELL
      end
      node.vm.provision :shell, inline: $script
    end
  end
end

$script = <<-SCRIPT
# スワップの無効化, ブリッジでのIPv4/IPv6トラフィックを有効
swapoff -a
cat <<EOF > /etc/sysctl.d/k8s.conf
net.bridge.bridge-nf-call-iptables = 1
net.bridge.bridge-nf-call-ip6tables = 1
vm.swappiness = 0
EOF
sysctl --system

# SELinuxの無効化
setenforce 0
sed -i 's/^SELINUX=.*/SELINUX=disabled/g' /etc/selinux/config

# dockerインストール
yum remove docker docker-client docker-client-latest docker-common \
           docker-latest docker-latest-logrotate docker-logrotate docker-engine
yum install -y yum-utils device-mapper-persistent-data lvm2
yum-config-manager --add-repo https://download.docker.com/linux/centos/docker-ce.repo
yum makecache fast
yum update -y
yum install -y docker-ce-18.09.8-3.el7.x86_64 docker-ce-cli-18.09.8-3.el7.x86_64 containerd.io
mkdir -p /etc/docker
cat <<EOF > /etc/docker/daemon.json
{
  "exec-opts": ["native.cgroupdriver=systemd"],
  "log-driver": "json-file",
  "log-opts": {
    "max-size": "100m"
  },
  "storage-driver": "overlay2",
  "storage-opts": ["overlay2.override_kernel_check=true"]
}
EOF
systemctl enable docker
systemctl daemon-reload
systemctl start docker

# kubectl, kubelet, kubeadmインストール
cat <<EOF > /etc/yum.repos.d/kubernetes.repo
[kubernetes]
name=Kubernetes
baseurl=https://packages.cloud.google.com/yum/repos/kubernetes-el7-x86_64
enabled=1
gpgcheck=1
repo_gpgcheck=1
gpgkey=https://packages.cloud.google.com/yum/doc/yum-key.gpg https://packages.cloud.google.com/yum/doc/rpm-package-key.gpg
exclude=kube*
EOF
yum install -y kubelet kubeadm kubectl --disableexcludes=kubernetes

# GlusterFSクライアントのインストール
yum install -y centos-release-gluster
yum install -y glusterfs-fuse
SCRIPT
```
* 仮想サーバのCPU, メモリ, ディスクサイズは適当な値です。
* Vagrantで構築した仮想サーバは、パスワード認証が無効化されており、秘密鍵を使ったssh接続しかできない。Kubernetesの構築手順では各ノード間をsshで接続できる必要があるそうなので、パスワード認証を有効にするとか各仮想サーバの秘密鍵を交換するかしなければならない。  
そこで「config.ssh.insert_key = false」により仮想サーバ毎に秘密鍵を生成されるのを無効化し、Vagrantが仮想サーバ構築時に使用している物理ホスト上の秘密鍵(${HOME}/.vagrant.d/insecure_private_key)を、各仮想サーバの秘密鍵としてしまう。各仮想サーバに公開鍵は登録されているので、これにより各仮想サーバ同士でssh接続が可能になる。

##### 仮想サーバを作成
```shell-session
# vagrant up
```
~~また、仮想サーバで使用しているDNSを物理ホストのresolve.confに登録。~~
(2019/8/26追記)仮想サーバのアドレスを物理ホストのhostsファイルに追記。

```shell-session
# cat <<EOF >> /etc/hosts
192.168.122.2 k8s-master1
192.168.122.3 k8s-master2
192.168.122.10 k8s-worker1
192.168.122.11 k8s-worker2
EOF
```
これで物理ホストから仮想サーバの名前解決ができるようになる。

##### kube-apiserver用ロードバランサーを作成

```shell_session
# cat <<EOF > /etc/yum.repos.d/nginx.repo
[nginx]
name=nginx repo
baseurl=http://nginx.org/packages/centos/7/\$basearch/
gpgcheck=0
enabled=1
EOF
# yum install -y nginx
# yum install -y policycoreutils-python
# semanage port -a -t http_port_t -p tcp 6443
# firewall-cmd --add-port=6443/tcp --zone=public --permanent
# firewall-cmd --reload
# cat <<EOF >> /etc/nginx/nginx.conf
stream {
    upstream backend {
        least_conn;
        server k8s-master1:6443;
        server k8s-master2:6443;
    }
    server {
        listen     6443;
        proxy_pass backend;
    }
}
EOF
# systemctl enable nginx
# systemctl start nginx
```
* nginxに慣れてなくて、ここは比較的ハマりました。Kubernetes実践ガイドにある設定例を真似して/etc/nginx/conf.d配下に記述したのですが、これだとデフォルトのnginx.confだとhttpの設定として扱われてしまうんですね。そこでnginx.confで直接stream {} で括って記述しました。
* 物理ホストですがselinuxやfirewalldを無効化してません。なのでkube-apiserverと同じポートを使えるようにしています。

### マスタノード1の構築
ここからはマスタノード1にログインして作業します。

##### Kubernetesの初期構築
CNIはcalicoを使うとして、calicoのデフォルトネットワークである192.168.0.0/16だとKVM側(192.168..121)と被ってしまうので、calico側を変更しました。

```shell_session
# cat <<EOF > kubeadm-config.yaml
apiVersion: kubeadm.k8s.io/v1beta1
kind: ClusterConfiguration
kubernetesVersion: stable
apiServer:
  certSANs:
  - "api.kubernetes.local"
controlPlaneEndpoint: "api.kubernetes.local:6443"
networking:
  podSubnet: "192.168.128.0/17"
EOF
# kubeadm init --config=kubeadm-config.yaml
... 中略 ...
Your Kubernetes control-plane has initialized successfully!

To start using your cluster, you need to run the following as a regular user:

  mkdir -p $HOME/.kube
  sudo cp -i /etc/kubernetes/admin.conf $HOME/.kube/config
  sudo chown $(id -u):$(id -g) $HOME/.kube/config

You should now deploy a pod network to the cluster.
Run "kubectl apply -f [podnetwork].yaml" with one of the options listed at:
  https://kubernetes.io/docs/concepts/cluster-administration/addons/

You can now join any number of control-plane nodes by copying certificate authorities 
and service account keys on each node and then running the following as root:

  kubeadm join api.kubernetes.local:6443 --token nogauk.ges57vw6ilmj8k9t \
    --discovery-token-ca-cert-hash sha256:c40c63e6c1b973c6918506b9bf7a6dd76d38d0a50ea2f9106ca9b9459c754159 \
    --control-plane 	  

Then you can join any number of worker nodes by running the following on each as root:

kubeadm join api.kubernetes.local:6443 --token nogauk.ges57vw6ilmj8k9t \
    --discovery-token-ca-cert-hash sha256:c40c63e6c1b973c6918506b9bf7a6dd76d38d0a50ea2f9106ca9b9459c754159 
# mkdir -p $HOME/.kube
# sudo cp -i /etc/kubernetes/admin.conf $HOME/.kube/config
# sudo chown $(id -u):$(id -g) $HOME/.kube/config
# curl -L -O https://docs.projectcalico.org/v3.8/manifests/calico.yaml
# sed -i 's:192.168.0.0/16:192.168.128.0/17:g' ./calico.yaml
# kubectl apply -f ./calico.yaml
```
* kubeadm-config.yamlに指定したapi.kubernetes.localは物理ホストのホスト名。podSubnetがcalicoに使ってもらうネットワーク。
* kubeadm initが成功すると、クラスタを利用するための初期手順、マスタノードをクラスタに追加するコマンド、ワーカーノードを追加するコマンドが表示されるのでメモしておく。
* 初期手順を実行したら、calicoのマニフェストをダウンロードして、calicoが使うネットワークを修正して適用。

##### マスタノード1の証明書をマスタノード2にコピー
Vagrantfileで秘密鍵はコピーしていたけど、それはvagrantユーザ用で、でもvagrantユーザでは証明書にアクセスできない。そこでrootユーザに秘密鍵をコピーしてから証明書の配布を実施。

```shell-session
# cp /home/vagrant/.ssh/id_rsa ~/.ssh
# USER=vagrant
# CONTROL_PLANE_IPS="k8s-master2"
# for host in ${CONTROL_PLANE_IPS}; do
     sudo scp /etc/kubernetes/pki/ca.crt ${USER}@$host:;
     sudo scp /etc/kubernetes/pki/ca.key ${USER}@$host:;
     sudo scp /etc/kubernetes/pki/sa.key ${USER}@$host:;
     sudo scp /etc/kubernetes/pki/sa.pub ${USER}@$host:;
     sudo scp /etc/kubernetes/pki/front-proxy-ca.crt ${USER}@$host:;
     sudo scp /etc/kubernetes/pki/front-proxy-ca.key ${USER}@$host:;
     sudo scp /etc/kubernetes/pki/etcd/ca.crt ${USER}@$host:etcd-ca.crt;
     sudo scp /etc/kubernetes/pki/etcd/ca.key ${USER}@$host:etcd-ca.key;
     sudo scp /etc/kubernetes/admin.conf ${USER}@$host:;
 done
```

### マスタノード2の構築
次にマスタノード2にログインしてrootユーザで以下を実施。
##### 証明書の適用

```shell-session
# USER=vagrant
# mkdir -p /etc/kubernetes/pki/etcd
# mv /home/${USER}/ca.crt /etc/kubernetes/pki/
# mv /home/${USER}/ca.key /etc/kubernetes/pki/
# mv /home/${USER}/sa.pub /etc/kubernetes/pki/
# mv /home/${USER}/sa.key /etc/kubernetes/pki/
# mv /home/${USER}/front-proxy-ca.crt /etc/kubernetes/pki/
# mv /home/${USER}/front-proxy-ca.key /etc/kubernetes/pki/
# mv /home/${USER}/etcd-ca.crt /etc/kubernetes/pki/etcd/ca.crt
# mv /home/${USER}/etcd-ca.key /etc/kubernetes/pki/etcd/ca.key
# mv /home/${USER}/admin.conf /etc/kubernetes/admin.conf
```
##### マスタノード2をクラスタに参加

```shell-session
#   kubeadm join api.kubernetes.local:6443 --token nogauk.ges57vw6ilmj8k9t \
    --discovery-token-ca-cert-hash sha256:c40c63e6c1b973c6918506b9bf7a6dd76d38d0a50ea2f9106ca9b9459c754159 \
    --control-plane 
```
~~※手元のログだと--control-planeだけど、ドキュメントとかだと--experimental-control-planeなので、別途、要確認。~~
* (2019/8/26追記)ドキュメントだと--experimental-control-planeだけど、kubeadm initで出力されるログには --control-plane とあるので、それを使用。

### ワーカーノードの構築
各ワーカーノードにログインしてrootユーザで以下を実施。
##### ワーカノードをクラスタに参加
```shell-session
# kubeadm join api.kubernetes.local:6443 --token nogauk.ges57vw6ilmj8k9t --discovery-token-ca-cert-hash sha256:c40c63e6c1b973c6918506b9bf7a6dd76d38d0a50ea2f9106ca9b9459c754159
```

### hello worldアプリケーションの実行
マスタノード1のrootユーザで以下を実行。

```shell-session
# kubectl create deployment hello-node --image=gcr.io/hello-minikube-zero-install/hello-node
# kubectl expose deployment hello-node --type=LoadBalancer --port=8080

# kubectl get service
NAME         TYPE           CLUSTER-IP      EXTERNAL-IP   PORT(S)          AGE
hello-node   LoadBalancer   10.102.47.188   <pending>     8080:32223/TCP   3h38m
kubernetes   ClusterIP      10.96.0.1       <none>        443/TCP          40h

# kubectl get pod
NAME                          READY   STATUS    RESTARTS   AGE
hello-node-55b49fb9f8-xfxgk   1/1     Running   0          3h38m
# kubectl get po/hello-node-55b49fb9f8-xfxgk -o jsonpath='{.status.hostIP}'
192.168.121.208
# kubectl get po/hello-node -o jsonpath='{.spec.ports[0].nodePort}'
32223

# curl http://192.168.121.208:32223
Hello World!
```
* kubectl createでhello worldアプリケーションをデプロイ。
* kubectl exposeでサービスを設定。ingressとかまだだから、EXTERNAL-IPはpending。なのでワーカーノードに直接curlで動作を確認しました。