Kubernetes 1.16をオンプレ環境にインストールしてみた


Kubernetes `v1.16.0`がリリースされて1週間ほど経ちましたが、手元の環境でインストールしてみました。折角なので記事として残してみようと思います。

## 構成

パブリッククラウド上で簡単にKubernetesを利用できる昨今ではありますが、それだとあまり面白くないので、家のPC上に仮想マシンを建てて作りたいと思います。

### クラスタ構成

今回は折角なのでMulti-Master構成で構築します。複数ノードにしても物理ホストが1台では意味ないよねとか言う天使の囁きは無視します。

Masterノード: 3台
Workerノード: 3台

今回のレベル感であればSingle-Master構成やMaster/Worker同居の完全1台構成に変更することも難しくはないので、補足的に随所に記載しておきます (`v1.15.3`の時の実績からなので、`v1.16.0`で手順が変わっていたらすみません)。

### ソフトウェア構成

今回はCNIに[flannel](https://github.com/coreos/flannel)を利用します。Workerが複数あるのにサービスの外部公開がNodePortでは残念過ぎるので、[MetalLB](https://metallb.universe.tf/)を利用してLoadBalancerを利用できるようにします。
本来であればMasterノードはHAProxyなどを利用してロードバランスされるべきですが、今回は(面倒だったので)keepaliveでAct-Stb-Stb状態で構築します。
また本当は[Dynamic Volume Provisioning](https://kubernetes.io/ja/docs/concepts/storage/dynamic-provisioning/)を利用できるように[GlusterFS](https://www.gluster.org/)も導入しようと思ったのですが、時間の都合により割愛します。その内、別記事で記載するかもしれません。

| Module    | Version |
| :-------- | ------: |
| CentOS    |     7.6 |
| Docker    | 19.03.2 |
| kubelet   |  1.16.0 |
| kubeadm   |  1.16.0 |
| kubectl   |  1.16.0 |
| keepalive |   1.3.5 |
| flannel   | v0.11.0 |
| MetalLB   |   0.8.1 |

### サイジング

作って動けば良いレベルなので、ホスト側のリソース量と相談して決めました。物理ホスト側は5〜6年くらい前に作った自作PCなので、そんなに高いスペックは必要ありません。ちなみに公式の推奨スペックは[こちら](https://kubernetes.io/ja/docs/setup/independent/install-kubeadm/#before-you-begin)です。

- Master/Worker共通
  - CPU: 2コア
  - メモリ: 4GB
  - ディスク: 64GB

ちなみに`v1.15.3`ですがMaster/Worker同居でもこのスペックでそこそこ動きます。

### ネットワーク構成

IPアドレス構成は以下の通りです。ホスト名の連番がバラバラなのは我が家の他のクラスタとの兼ね合いなので気にしないでください。`k8s-master02`を最初に構築するMasterとして、`k8s-master*`でMaserクラスタを構築します。`k8s-worker*`がWorkerノード用のVMです。
flannelのIPはCNIによって要件が変わるので、他のCNIを利用する場合は[ドキュメント](https://kubernetes.io/ja/docs/setup/independent/create-cluster-kubeadm/#pod-network)を参照して変更してください。

| 役割                     |      IPアドレス |
| :----------------------- | --------------: |
| k8s-master02             |    192.168.2.15 |
| k8s-master03             |    192.168.2.16 |
| k8s-master04             |    192.168.2.17 |
| MasterノードのクラスタIP |    192.168.2.18 |
| k8s-worker04             |    192.168.2.24 |
| k8s-worker05             |    192.168.2.25 |
| k8s-worker06             |    192.168.2.26 |
| flannelのIP              |   10.244.0.0/16 |
| ClusterIP                |    10.96.0.0/12 |
| LoadBalancerIP           | 192.168.2.64/27 |

## インストール

### CentOSのインストール (`k8s-master02`)

まずは最初のMasterノードとなる`k8s-master02`のCentOSをminimalインストールします。他のノードは`k8s-master02`をクローンして作成するので、この時点では構築不要です。インストール時にネットワーク設定とユーザ作成を行っています。ユーザは`rltnm7`というユーザにしました。

### OSの設定 (`k8s-master02`)

既にSSH出来るはずなので、SSHで外部から繋いで設定しています。(面倒だったので)しばらくは`root`ユーザで設定しています。ユーザ設定などは各自必要に応じて設定してください。

firewallを無効化します。真面目にやる時はちゃんと設定しましょう。

```sh
systemctl stop firewalld
systemctl disable firewalld
```

selinuxも無効化します。

```sh
setenforce 0
sed -i "s/SELINUX=enforcing/SELINUX=disabled/" /etc/selinux/config
```

swapを無効化します。これは必須です。やらないと設定時のpreflightで失敗します。

```sh
swapoff -a
sed -i "s/\/dev\/mapper\/centos-swap/#\/dev\/mapper\/centos-swap/" /etc/fstab
```

hostsの追加をします。ちゃんとDNSがある環境であれば不要な設定かと思います。

```sh
cat << EOF >> /etc/hosts
192.168.2.15 k8s-master02
192.168.2.16 k8s-master03
192.168.2.17 k8s-master04
192.168.2.24 k8s-worker04
192.168.2.25 k8s-worker05
192.168.2.26 k8s-worker06
EOF
```

yumのアップデートもしておきます。OSのイメージが古いので300以上updateが走りました。

```sh
yum update -y
yum upgrade -y
```

### Dockerのインストール (`k8s-master02`)

DockerのCommunity Editionを使います。`yum install docker`するとEnterprise Edtionがインストールされるので気をつけてください。エディションの違いは[こちら](http://docs.docker.jp/engine/installation/)を参照してください。

```sh
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 install -y docker-ce
```

`rltnm7`ユーザを`docker`グループに追加します。これをしないと`rltnm7`ユーザが`docker`コマンドを実行する際に毎回`sudo`をしなければなりません。Kubernetesを利用する上で必須の設定ではありませんが、トラブルシュートする際に設定されていると便利です。

```sh
usermod -g docker rltnm7
```

こちらの設定も必須ではありませんが、ローカル環境にPrivate Registryを構築し、かつセキュリティ設定が面倒でやらない場合に設定する必要がある項目です。このRegistryはHTTPSではなくHTTPでアクセスしてねっていう設定です。Docker Hubしか使わないのであれば不要です。

```sh
cat << EOF > /etc/sysconfig/docker
INSECURE_REGISTRY='--insecure-registry registry:5000'
EOF
```

最後にDockerを起動します。

```sh
systemctl start docker
systemctl enable docker
```

### Kubernetesのインストール (`k8s-master02`)

インストール前にネットワーク設定をしておきます。

```sh
cat << EOF > /etc/sysctl.d/k8s.conf
net.bridge.bridge-nf-call-ip6tables = 1
net.bridge.bridge-nf-call-iptables = 1
EOF

sysctl --system
```

ついにKubernetesをインストールします。Kubernetes用のyumレポジトリを追加して、インストールします。あえてバージョン指定しましたが、最新版を入れるのであれば指定は不要です。

```sh
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
EOF

yum install -y kubelet-1.16.0-0.x86_64 kubeadm-1.16.0-0.x86_64 kubectl-1.16.0-0.x86_64
```

最後にkubeletの自動起動設定を入れて、シャットダウンします。すぐに落としてしまうので起動はしていませんが、`systemctl start kubelet`して動作確認してみても良いかもしれませんね。Kubenetes自体はまだ機能していないので何が見れるか分かりませんが。。。

```sh
systemctl enable kubelet
shutdown -h now
```

### VMのクローン (`k8s-master02`以外全て)

ここまで構築したVMをクローンします。Master用に2つ、Worker用に3つクローンします。Master用はもう少しだけ共通の設定をしますが、本当に少しなのでこの時点でクローンしてしまいます。クローン手順は省略します。VirtualBoxであれば右クリックでクローン選ぶだけです。
もちろんSingle-Masterにする場合はMasterのクローンは不要ですし、1台構成にするのであればWorkerのクローンも不要です。

### 各VMの設定 (`k8s-master02`以外全て)

クローンしたVMのOS設定をしていきます。最初はIP競合を起こしてしまうので、各VMを**1台ずつ**起動し、以下の設定をして落としていきます。ホスト名/IPアドレスは対象マシンごとに変更してください。

```sh
sed -i "s/k8s-master02/k8s-master03/" /etc/hostname
sed -i "s/192.168.2.15/192.168.2.16/" /etc/sysconfig/network-scripts/ifcfg-enp0s3
shutdown -h now
```

### Keepaliveの設定 (全てのMasterノード)

Masterノードに仮想IPを設定します。`kubectl`などが接続するIPアドレスになります。priorityは優先順位が高い順に変更してください。私は`k8s-master02, 03, 04`の順に`100, 90, 80`としました。
Single-Master/1台構成の場合は不要な設定です。

```sh
yum install -y keepalived
mv /etc/keepalived/keepalived.conf /etc/keepalived/keepalived.conf.org

cat <<EOF > /etc/keepalived/keepalived.conf
global_defs {
}

vrrp_instance KUBE_APISERVER {
    state MASTER
    interface enp0s3
    virtual_router_id 51
    priority 100
    advert_int 1
    virtual_ipaddress {
        192.168.2.18/24
    }
}
EOF

systemctl start keepalived
systemctl enable keepalived
```

### 最初のMasterの設定 (`k8s-master02`)

ようやくMasterノードの構築に入ります。`kubeadm init`コマンドを実行するだけの簡単なお仕事です。
`--apiserver-advertise-address`および`--control-plane-endpoint`に対してはkeepaliveで設定したクラスタIPを渡します。`--pod-network-cidr`にはflannelのIPを渡します。こちらはCNIによって異なるので利用するCNIのドキュメントを参照してください。あるいは不要な場合もあるかもしれません。`--service-cidr`はClusterIPのIPアドレス範囲になります。デフォルトで`10.96.0.0/12`になるので、省略しても大丈夫なはずです(試したことないですが)。変えたい場合は設定してください。
またSingle-Masterや1台構成の場合は`--control-plane-endpoint`の引数が不要になります。

```sh
kubeadm init --apiserver-advertise-address 192.168.2.18  --control-plane-endpoint 192.168.2.18 --pod-network-cidr 10.244.0.0/16 --service-cidr 10.96.0.0/12
```

最初にpreflightでシステム要件の確認をされます。何か足りなければここでエラーが出てインストールが失敗します。
`kubeadm init`の結果に`kubeadm join`コマンドが2つ(`--control-plane-endpoint`を指定していない場合は1つ)記載されているので**メモしておいてください**。

### Master間の鍵交換

Kubernetesの鍵情報を送りつけるために、まずはSSHの鍵を作成し、公開鍵を送ります。これは`k8s-master02`でのみ実行します。

```sh
ssh-keygen -t rsa -b 4096 -C "admin@example.com"

USER=rltnm7
scp .ssh/id_rsa.pub "${USER}"@192.168.2.16:
scp .ssh/id_rsa.pub "${USER}"@192.168.2.17:
```

受け取り手側で公開鍵を受け入れます。これは`k8s-master03`と`k8s-master04`の一般ユーザ`rltnm7`で実行します。全部`root`でやり取りでも良かったのですが、何となく`root`でダイレクトにSSHするのは気が引けたので。。。

```sh
mkdir .ssh
chmod 700 .ssh
cd .ssh
cat ~/id_dsa.pub >> authorized_keys
chmod 600 authorized_keys

sudo systemctl restart sshd
```

`k8s-master02`に戻ってSCPでファイルを送付します。一般ユーザだとファイルにアクセス権がないので`root`で実行します。実行する前にSSHでパスワードなしでアクセスできるか試しておいた方が幸せになれると思います。SSHの鍵交換は最悪やらなくても、下記SCPの際に(9*台数)回パスワード入力すれば大丈夫です笑

```sh
USER=rltnm7
CONTROL_PLANE_IPS="192.168.2.16 192.168.2.17"
for host in ${CONTROL_PLANE_IPS}; do
    scp /etc/kubernetes/pki/ca.crt "${USER}"@$host:
    scp /etc/kubernetes/pki/ca.key "${USER}"@$host:
    scp /etc/kubernetes/pki/sa.key "${USER}"@$host:
    scp /etc/kubernetes/pki/sa.pub "${USER}"@$host:
    scp /etc/kubernetes/pki/front-proxy-ca.crt "${USER}"@$host:
    scp /etc/kubernetes/pki/front-proxy-ca.key "${USER}"@$host:
    scp /etc/kubernetes/pki/etcd/ca.crt "${USER}"@$host:etcd-ca.crt
    scp /etc/kubernetes/pki/etcd/ca.key "${USER}"@$host:etcd-ca.key
    scp /etc/kubernetes/admin.conf "${USER}"@$host:
done
```

### `kubectl`の設定

`kubectl`を利用できるように設定します。ここからは`root`ユーザではなく、一般ユーザで設定を行います。今回は既に作成済みの`rltnm7`を利用します。
この時点では`k8s-master02`でしか出来ませんが、後で他のMasterでもやっておいた方が良いと思います。

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

さて、`kubectl`コマンドが利用できるようになったので、試しにいくつか実行してみましょう。

```sh
kubectl get nodes
```

`k8s-master02`が`NotReady`状態になっていると思います。

```sh
kubectl get pod --namespace kube-system
```

CoreDNSが`Pending`状態で、その他のPodは`Running`になっていると思います。まだなっていなければ少し待ってからもう一度確認してみてください。CoreDNSはCNIを展開しない限りは永遠に`Pending`のままなので先に進んでください。

### CNIの設定 (`k8s-master02`)

クラスタ上へflannelを展開します。github上のmanifestファイルを直接指定しています。
注意点としてKubernetes `v1.16.0`から[DaemonSetのAPIがapps/v1beta1からapps/v1へ変更](https://github.com/kubernetes/kubernetes/blob/master/CHANGELOG-1.16.md#deprecations-and-removals)になっているので、`1.15`以前に使えていたmanifestファイルは使えません。

```sh
kubectl apply -f https://raw.githubusercontent.com/coreos/flannel/2140ac876ef134e0ed5af15c65e414cf26827915/Documentation/kube-flannel.yml
```

参考までに`v1.15.3`の場合は、以下のコミットハッシュのファイルで展開していました。(最初にこちらを実行して失敗したのはナイショ)

```sh
kubectl apply -f https://raw.githubusercontent.com/coreos/flannel/62e44c867a2846fefb68bd5f178daf4da3095ccb/Documentation/kube-flannel.yml
```

環境に依るとも思いますが、5〜10分くらい待ってからもう一度`kubectl get pod --namespace kube-system`を実行すると全てのPodが`Running`になっていると思います。たまに`Restart`してたりしますが、その内収束するので焦らずお待ち下さい。
待っている間は折角なので`v1.16.0`からの新機能`kubectl get node -n kube-system -w --output-watch-events`なんか使って、Podの作成状況なんか眺めてみると面白いかもしれませんね。
最後に`kubectl get nodes`を実行して`k8s-master02`が`Ready`になっていればOKです。

さて、ここまで来れば`k8s-master02`は完成です。

### 残りのMasterの設定 (`k8s-master03`, `k8s-master04`)

先程SCPした鍵を所定の場所に移動します。こちらは`root`で実行するか、`sudo`でやってください。
Single-Master/1台構成の場合はこの説は飛ばしてしまって大丈夫です。

```sh
USER=rltnm7
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
```

最後に、`k8s-master02`にて構成したクラスタに参加します。`kubeadm init`した際にメモした`kubeadm join`コマンドを実行します。Masterでは`--control-plane`引数が**付いている方**を実行します。付いていない方を実行してしまうと、ただのWorkerになってしまうのでご注意ください。

```sh
sudo kubeadm join 192.168.2.18:6443 --token lpm579.cxre2a1xzucdp547 --discovery-token-ca-cert-hash sha256:6c2d80e1b4d26f12c48ba83b8a7b9d502a0e3877dc0cd29130b562925588e1be --control-plane
```

`k8s-master03`、`k8s-master04`でそれぞれ完了したら、`kubectl get nodes`を実行し、全てのNodeが`Ready`になればMulti-Master構成は完成です。前述の通り、各Masterで`kubectl`の設定をしておくと良いと思います。続いてWorkerの設定を行います。

### Workerの設定 (全てのWorker)

1台構成の場合はこの節も飛ばして大丈夫です。代わりに[Master/Worker同居1台構成の場合](#masterworker%e5%90%8c%e5%b1%851%e5%8f%b0%e6%a7%8b%e6%88%90%e3%81%ae%e5%a0%b4%e5%90%88)を実施してください。

まずはIPv4の転送設定を行います。

```sh
sudo sh -c "echo 'net.ipv4.ip_forward = 1' >> /etc/sysctl.d/k8s.conf"
sudo sysctl --system
```

次に`k8s-master02`にて構成したクラスタに参加します。`kubeadm init`した際にメモした`kubeadm join`コマンドを実行します。Workerでは`--control-plane`引数が**付いていない方**を実行します。

```sh
sudo kubeadm join 192.168.2.18:6443 --token lpm579.cxre2a1xzucdp547 --discovery-token-ca-cert-hash sha256:6c2d80e1b4d26f12c48ba83b8a7b9d502a0e3877dc0cd29130b562925588e1be
```

`kubectl get nodes`を実行し、対象のノードが`Ready`になったらOKです。全てのWorkerノードの追加が完了したらKubernetesクラスタの完成です。

### Master/Worker同居1台構成の場合

Master上では通常、ユーザが作成したコンテナは実行できないので、Master/Workerを同居させる場合には追加の設定が必要になります。ちなみに同居させることは、あまり推奨できる構成ではないので開発や検証程度にしておいた方が良いでしょう。
**MasterとWorkerを分けた構成にする場合は不要な設定です。**

まずはIPv4の転送設定を行います。

```sh
sudo sh -c "echo 'net.ipv4.ip_forward = 1' >> /etc/sysctl.d/k8s.conf"
sudo sysctl --system
```

次にMasterノードでもコンテナを実行できるようにします。

```sh
kubectl taint node k8s-master02 node-role.kubernetes.io/master:NoSchedule-
```

これでMaster/Worker同居のクラスタが完成しました。

### MetalLBの導入

さて、ここまでで無事にKubernetesクラスタは完成し、Kubernetesとして最低限利用できるようにはなりました。しかしまだ`LoadBalancer`サービスは利用できません。そこでクラスタ上にMetalLBをデプロイして`LoadBalancer`サービスを利用できるようにします。

まずはデプロイします。

```sh
kubectl apply -f https://raw.githubusercontent.com/google/metallb/v0.8.1/manifests/metallb.yaml
```

MetalLBにはL2モードとBGPモードががありますが、今回はL2モードで実行させます。違いは[ドキュメント](https://metallb.universe.tf/configuration/)を参照してください。`addresses`に設定されているIPアドレスが`LoadBalancer`の`ExternalIP`として設定されるIPアドレス範囲になります。

```sh
cat << EOF > metallb-l2.yaml
apiVersion: v1
kind: ConfigMap
metadata:
  name: config
  namespace: metallb-system
data:
  config: |
    address-pools:
    - name: default
      protocol: layer2
      addresses:
      - 192.168.2.64/27
EOF

kubectl apply -f metallb-l2.yaml
```

### その他

その他、IngressやIstio、Dynamic Provisioningなどで遊びたいところではありますが、今回の構築範囲としてはここまでとし、またいつか別記事にしたいと思います。

## 動作確認

だいぶ長くなっていますが、作って終わりも面白くないので、少し遊んでみたいと思います。

### コンテナのデプロイ

とても雑に`type: LoadBalancer`のサービスと`replica: 3`で`nginx`の`Deployment`を展開したいと思います。ただの`nginx`だと面白くないので`image: stenote/nginx-hostname:latest`として、ホスト名が出るようにします。

```sh
cat << EOF > nginx.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: nginx
spec:
  replicas: 3
  selector:
    matchLabels:
      app: nginx
  template:
    metadata:
      labels:
        app: nginx
    spec:
      containers:
      - name: nginx
        image: stenote/nginx-hostname:latest
        ports:
        - name: http
          containerPort: 80
---
apiVersion: v1
kind: Service
metadata:
  name: nginx
spec:
  type: LoadBalancer
  ports:
  - name: http
    port: 80
    protocol: TCP
    targetPort: 80
  selector:
    app: nginx
EOF

kubectl apply -f nginx.yaml
kubectl get pod -o wide -w --output-watch-events
```

全て`Running`になったのを確認したら、以下を実行します。IPは`kubectl get svc`を実行して、`nginx`の`External-IP`を指定してください。

```sh
for i in `seq 0 100`; do curl http://192.168.2.64; done
```

3台に分散されてホスト名 (= Pod名) が表示されると思います。
最後に掃除しておきます。

```sh
kubectl delete -f nginx.yaml
rm -f nginx.yaml
```

### Multi-Masterの動作を見る

特に記載できる内容もないのですが、、、
通常時はk8s-master02をAct機として動作していますが、k8s-master02をシャットダウンしても、k8s-master03をAct機として動作するのが分かると思います。
ただし今回はkeepaliveでのIP付け替えという雑なやり方しかしていないので、特に復旧時の切り替えには比較的長めの接続断が発生してしまいます。きれいな切り替えがやりたい場合はHAProxyを利用するなど、ロードバランス機能を持たせるべきでしょう。

## 感想

やはり自分で構築するとKubernetesへの理解が深まりますね。パブリッククラウドでは手軽に利用できる反面、上っ面だけでも使えてしまうので、こう言った自分の手を動かす機会も積極的に持ちたいですね。
`Ephemeral Containers`とかも遊びたかったのですが、気力が尽きたので終わります。メモリ24GBとか普通に邪魔なので、このクラスタは消して1台構成で作り直して遊んでみたいと思います。
Kubernetesインストール系の記事も最近はだいぶ多くなっているので、今更需要があるのかも分かりませんが、どなたかの一助となれば幸いです。今後も色々Kubernetesだけでなく、色々な記事を書いてみようと思います。