ESXi上のCentOS8にKubeadmでKubernetesクラスタを構築する

###### 更新
2020/07/19 Kubernetes 1.18.6, Calico 3.15.1 も同じ手順でインストール可能です。

#環境
ESXi7.0上にVM3台作成

- vm
  - k8s-master cpu:2 mem:4GB HDD:30GB IP:192.168.123.150
  - k8s-node1  cpu:2 mem:4GB HDD:30GB IP:192.168.123.151
  - k8s-node2  cpu:2 mem:4GB HDD:30GB IP:192.168.123.152

- OS
  - CentOS8.2.2004 Minimalインストール

- インストールするKubernetesのバージョン
  - 1.18.4

[Installing kubeadm](https://kubernetes.io/docs/setup/production-environment/tools/kubeadm/install-kubeadm/)によると、インストールするマシンは2GB以上のメモリ、cpu2コア以上が必要とのこと。

#はじめに
Kubernetesの勉強用に自前で環境を組んでみることにしました。
手っ取り早く環境を構築して動かしたい人向けです。
基本的に公式の[Bootstrapping clusters with kubeadm](https://kubernetes.io/docs/setup/production-environment/tools/kubeadm/)の通りに進めていますが、結構苦戦したので手順をまとめました。

#手順
1. 各ノードで最新のアップデートを適用する

    ```
    dnf update -y
    ```

1. 各ノードのswapをオフにする（ここでは設定のみ。後でノード再起動で反映させる。）
    swapをオフにし、/etc/fstabからswap領域のマウントの記載を削除する。

    ```
    # vi /etc/fstab
      → swapの行をコメントアウト
    ```

1. 各ノードで名前解決ができるようにする
    各ノードの /etc/hosts に以下の値を追加する

    ```
    192.168.123.150 k8s-master
    192.168.123.151 k8s-node1
    192.168.123.152 k8s-node2
    ```

1. 各ノードのMACアドレスと product_uuidが一意であることの確認
    - MACアドレス確認
    
        ```
        # ip link
        ```
    - product_uuid確認
        
        ```
        # cat /sys/class/dmi/id/product_uuid
        ```

1. 各ノードのFirewallの無効化（ここでは自動起動無効化のみ。後でノード再起動で反映させる。）

    ```
    # systemctl disable firewalld
    ```

1. 各ノードのselinuxの無効化（ここでは設定のみ。後でノード再起動で反映させる。）

    ```
    # vi /etc/selinux/config
    →SELINUX=permissive に変更
    ```

1. 各ノードのipv6無効化（ここでは設定のみ。後でノード再起動で反映させる。）

    ```
    # echo "net.ipv6.conf.all.disable_ipv6 = 1" >> /etc/sysctl.conf
    # echo "net.ipv6.conf.default.disable_ipv6 = 1" >> /etc/sysctl.conf
    ```

1. 各ノードのネットワーク・ブリッジを通過するパケットにiptablesを適用（ここでは設定のみ。後でノード再起動で反映させる。）

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

1. 各ノードのipv4のフォワード設定（ここでは設定のみ。後でノード再起動でまとめて反映させる。）

    ```
    # echo "net.ipv4.ip_forward = 1" >> /etc/sysctl.conf
    ```

1. これまでの設定を反映させるため、全ノードを再起動する

    ```
    # shutdown -r now
    ```

1. 各ノードでコンテナランタイムをインストール
Dockerをインストールします。
インストール手順は「[Install Docker Engine on CentOS](https://docs.docker.com/engine/install/centos/)」を参考にしています。
手順通りにインストールしようとするとエラーが発生したので、「[How to install Docker CE on RHEL 8 / CentOS 8](https://linuxconfig.org/how-to-install-docker-in-rhel-8)」を参考にインストール手順を少し変更しています。(CentOS8からはDockerコンテナエンジンが削除されてpodmanを使うようになったのが原因のようです。)

    ```
    # dnf config-manager --add-repo=https://download.docker.com/linux/centos/docker-ce.repo
    # dnf install https://download.docker.com/linux/centos/7/x86_64/stable/Packages/containerd.io-1.2.13-3.2.el7.x86_64.rpm -y
    # dnf install docker-ce docker-ce-cli -y
    # mkdir /etc/docker
    # cat > /etc/docker/daemon.json <<EOF
    {
      "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
    # mkdir -p /etc/systemd/system/docker.service.d
    # systemctl daemon-reload
    # systemctl enable --now docker
    ```

1. 各ノードでkubeadm, kubelet, kubectlをインストール
    Kubernetesのリポジトリを追加してkubeadm, kubelet, kubectlをインストールし、kubeletを起動します。
    この時点では、kubeletは起動＆失敗を繰り返しますが、次の手順のkubeadm initを実行することで正常に起動するようになります。
    公式の手順では、ここでSELinuxをpermissiveに変更していますが、事前に変更済みのため省略しています。

    ```
    # cat <<EOF > /etc/yum.repos.d/kubernetes.repo
    [kubernetes]
    name=Kubernetes
    baseurl=https://packages.cloud.google.com/yum/repos/kubernetes-el7-\$basearch
    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=kubelet kubeadm kubectl
    EOF
    # dnf install kubelet kubeadm kubectl --disableexcludes=kubernetes -y
    # systemctl enable --now kubelet
    ```

1. k8s-masterノードでkubeadmコマンドを使用してControl-plane nodeを初期化する
    - kubeadmコマンドのオプション
        - --apiserver-advertise-address：apiサーバのIPアドレス。マスターノードのIPアドレスを指定する。
        - --pod-network-cidr：Podに割り当てるIPアドレスの範囲。（ホストネットワークと重複しないように設定する）

    ```
    # kubeadm init --apiserver-advertise-address 192.168.123.150 --pod-network-cidr 10.240.0.0/16
    ```

    
    <details><summary>実行例</summary><div>
    
    ```
    [root@k8s-master ~]# kubeadm init --apiserver-advertise-address 192.168.123.150 --pod-network-cidr 10.240.0.0/16
    W0621 23:58:11.156994  134717 configset.go:202] WARNING: kubeadm cannot validate component configs for API groups [kubelet.config.k8s.io kubeproxy.config.k8s.io]
    [init] Using Kubernetes version: v1.18.4
    [preflight] Running pre-flight checks
            [WARNING Firewalld]: firewalld is active, please ensure ports [6443 10250] are open or your cluster may not function correctly
            [WARNING FileExisting-tc]: tc not found in system path
    [preflight] Pulling images required for setting up a Kubernetes cluster
    [preflight] This might take a minute or two, depending on the speed of your internet connection
    [preflight] You can also perform this action in beforehand using 'kubeadm config images pull'
    [kubelet-start] Writing kubelet environment file with flags to file "/var/lib/kubelet/kubeadm-flags.env"
    [kubelet-start] Writing kubelet configuration to file "/var/lib/kubelet/config.yaml"
    [kubelet-start] Starting the kubelet
    [certs] Using certificateDir folder "/etc/kubernetes/pki"
    [certs] Generating "ca" certificate and key
    [certs] Generating "apiserver" certificate and key
    [certs] apiserver serving cert is signed for DNS names [k8s-master kubernetes kubernetes.default kubernetes.default.svc kubernetes.default.svc.cluster.local] and IPs [10.96.0.1 192.168.123.150]
    [certs] Generating "apiserver-kubelet-client" certificate and key
    [certs] Generating "front-proxy-ca" certificate and key
    [certs] Generating "front-proxy-client" certificate and key
    [certs] Generating "etcd/ca" certificate and key
    [certs] Generating "etcd/server" certificate and key
    [certs] etcd/server serving cert is signed for DNS names [k8s-master localhost] and IPs [192.168.123.150 127.0.0.1 ::1]
    [certs] Generating "etcd/peer" certificate and key
    [certs] etcd/peer serving cert is signed for DNS names [k8s-master localhost] and IPs [192.168.123.150 127.0.0.1 ::1]
    [certs] Generating "etcd/healthcheck-client" certificate and key
    [certs] Generating "apiserver-etcd-client" certificate and key
    [certs] Generating "sa" key and public key
    [kubeconfig] Using kubeconfig folder "/etc/kubernetes"
    [kubeconfig] Writing "admin.conf" kubeconfig file
    [kubeconfig] Writing "kubelet.conf" kubeconfig file
    [kubeconfig] Writing "controller-manager.conf" kubeconfig file
    [kubeconfig] Writing "scheduler.conf" kubeconfig file
    [control-plane] Using manifest folder "/etc/kubernetes/manifests"
    [control-plane] Creating static Pod manifest for "kube-apiserver"
    [control-plane] Creating static Pod manifest for "kube-controller-manager"
    W0621 23:58:18.001687  134717 manifests.go:225] the default kube-apiserver authorization-mode is "Node,RBAC"; using "Node,RBAC"
    [control-plane] Creating static Pod manifest for "kube-scheduler"
    W0621 23:58:18.002962  134717 manifests.go:225] the default kube-apiserver authorization-mode is "Node,RBAC"; using "Node,RBAC"
    [etcd] Creating static Pod manifest for local etcd in "/etc/kubernetes/manifests"
    [wait-control-plane] Waiting for the kubelet to boot up the control plane as static Pods from directory "/etc/kubernetes/manifests". This can take up to 4m0s
    [apiclient] All control plane components are healthy after 21.503071 seconds
    [upload-config] Storing the configuration used in ConfigMap "kubeadm-config" in the "kube-system" Namespace
    [kubelet] Creating a ConfigMap "kubelet-config-1.18" in namespace kube-system with the configuration for the kubelets in the cluster
    [upload-certs] Skipping phase. Please see --upload-certs
    [mark-control-plane] Marking the node k8s-master as control-plane by adding the label "node-role.kubernetes.io/master=''"
    [mark-control-plane] Marking the node k8s-master as control-plane by adding the taints [node-role.kubernetes.io/master:NoSchedule]
    [bootstrap-token] Using token: u4t96u.malq5uoi0of545r6
    [bootstrap-token] Configuring bootstrap tokens, cluster-info ConfigMap, RBAC Roles
    [bootstrap-token] configured RBAC rules to allow Node Bootstrap tokens to get nodes
    [bootstrap-token] configured RBAC rules to allow Node Bootstrap tokens to post CSRs in order for nodes to get long term certificate credentials
    [bootstrap-token] configured RBAC rules to allow the csrapprover controller automatically approve CSRs from a Node Bootstrap Token
    [bootstrap-token] configured RBAC rules to allow certificate rotation for all node client certificates in the cluster
    [bootstrap-token] Creating the "cluster-info" ConfigMap in the "kube-public" namespace
    [kubelet-finalize] Updating "/etc/kubernetes/kubelet.conf" to point to a rotatable kubelet client certificate and key
    [addons] Applied essential addon: CoreDNS
    [addons] Applied essential addon: kube-proxy

    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/

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

    kubeadm join 192.168.123.150:6443 --token u4t96u.malq5uoi0of545r6 \
        --discovery-token-ca-cert-hash sha256:1700be62aefeb52188c53ff77e0ce43b329fec3837dc83b45bd9d7d649f767e2
    [root@k8s-master ~]#
    ```
    </div></details>

    上記実行例の出力にもある通り、一般ユーザでkubectl コマンドを実行する場合は、一般ユーザで以下のコマンドを実行する必要があります。

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

    rootユーザの場合は、以下のコマンドを実行すると、kubectlコマンドが実行できます。

    ```
    export KUBECONFIG=/etc/kubernetes/admin.conf
    ```

    私は以下のコマンドをrootで実行してrootコマンドでkubectlを実行できるようにしました。

    ```
    # mkdir -p $HOME/.kube
    # cp -pi /etc/kubernetes/admin.conf $HOME/.kube/config
    ```
    また、上記実行例の出力の最後に出力された以下の部分はノードを追加する際に使用するためメモしておいてください。

    ```
    kubeadm join 192.168.123.150:6443 u4t96u.malq5uoi0of545r6 \
        --discovery-token-ca-cert-hash sha256:1700be62aefeb52188c53ff77e0ce43b329fec3837dc83b45bd9d7d649f767e2
    ```


1. Container Network Interface(CNI)のインストール
    kubectl get all -Aでリソースを表示すると、CoreDNSが動作していません。CNIをインストールしないと動作しません（「[Installing a Pod network add-on](https://kubernetes.io/docs/setup/production-environment/tools/kubeadm/create-cluster-kubeadm/#pod-network)」）。

    ```
    [root@k8s-master ~]# kubectl get all -A
    NAMESPACE     NAME                                     READY   STATUS              RESTARTS   AGE
    kube-system   pod/coredns-66bff467f8-dp8bl             0/1     Pending             0          62s
    kube-system   pod/coredns-66bff467f8-sqsgg             0/1     Pending             0          62s
    :
    NAMESPACE     NAME                      READY   UP-TO-DATE   AVAILABLE   AGE
    kube-system   deployment.apps/coredns   0/2     2            0           78s
    :
    ```

    いくつか選択肢があるようですが、今回は「Calico」をインストールします。
    現時点での最新版となる3.15をインストールしました。

    NetworkManagerがインターフェースに干渉しないように、`/etc/NetworkManager/conf.d/calico.conf`を作成します。
    (Configure NetworkManager)[https://docs.projectcalico.org/maintenance/troubleshoot/troubleshooting#configure-networkmanager]

    全ノードで作成します。

    ```
    cat <<EOF > /etc/NetworkManager/conf.d/calico.conf
    [keyfile]
    unmanaged-devices=interface-name:cali*;interface-name:tunl*;interface-name:vxlan.calico
    EOF
    ```

    k8s-masterノードでCalioをインストール(Calio 3.15.1も同じURLでインストールできます)

    ```
    # kubectl apply -f https://docs.projectcalico.org/v3.15/manifests/calico.yaml
    ```
    しばらく待つと、corednsおよびcalicoがRunningになります。`kubectl get pods -n kube-system`で確認できます。


1. Kubernetesクラスタにノードを追加する
    k8s-node1, k8s-node2で以下のコマンドを実行し、Kubernetesクラスタに各ノードを追加します。

    ```
    # kubeadm join 192.168.123.150:6443 --token u4t96u.malq5uoi0of545r6 \
        --discovery-token-ca-cert-hash sha256:1700be62aefeb52188c53ff77e0ce43b329fec3837dc83b45bd9d7d649f767e2
    ```

    追加されたかどうかは`kubectl get nodes`で確認できます。

    ```
    [root@k8s-master ~]# kubectl get nodes
    NAME         STATUS   ROLES    AGE   VERSION
    k8s-master   Ready    master   42m   v1.18.4
    k8s-node1    Ready    <none>   68s   v1.18.4
    k8s-node2    Ready    <none>   26s   v1.18.4
    [root@k8s-master ~]#
    ```

    しばらくすると、coredns、追加されたノード用のcalico-nodeの起動が完了します。

    ```
    [root@k8s-master ~]# kubectl get pods -n kube-system
    NAME                                       READY   STATUS    RESTARTS   AGE
    calico-kube-controllers-58b656d69f-4kd5f   1/1     Running   0          64m
    calico-node-2w98n                          1/1     Running   0          62m
    calico-node-cqw29                          1/1     Running   0          64m
    calico-node-fh2zw                          1/1     Running   0          61m
    coredns-66bff467f8-st5k8                   1/1     Running   0          65m
    coredns-66bff467f8-zk27f                   1/1     Running   0          65m
    :
    ```

1. 動作確認
    [Kubernetes pods by example](https://kubernetesbyexample.com/pods/)を参考にして作ったPodとサービス(NodePort)を動かします。


    1. k8s-master で以下のコマンドを実行してnginxのサンプルを動かします。

        ```
        # kubectl apply -f https://raw.githubusercontent.com/yasubehe/k8s-test001/master/sample-nginx-pod.yaml
        # kubectl get pods -l run=my-nginx -o wide
        # kubectl apply -f https://raw.githubusercontent.com/yasubehe/k8s-test001/master/sample-nginx-service-nodeport.yaml
        # kubectl describe svc my-nginx
        ```

        起動したことを確認します。

        ```
        [root@k8s-master ~]# kubectl get pods -l run=my-nginx -o wide
        NAME                        READY   STATUS    RESTARTS   AGE   IP               NODE        NOMINATED NODE   READINESS GATES
        my-nginx-5dc4865748-64l7d   1/1     Running   0          43s   10.240.169.130   k8s-node2   <none>           <none>
        my-nginx-5dc4865748-6lxhv   1/1     Running   0          43s   10.240.36.66     k8s-node1   <none>           <none>
        [root@k8s-master ~]# kubectl get svc my-nginx
        NAME       TYPE       CLUSTER-IP       EXTERNAL-IP   PORT(S)          AGE
        my-nginx   NodePort   10.111.252.182   <none>        8080:30080/TCP   36s
        [root@k8s-master ~]#
        ```
    
    1. 別のターミナルを2つ用意し、k8s-master にログインしてmy-nginxのpodのログを監視します。

        `kubectl get pods -l run=my-nginx -o wide`に出力されたPodのNAMEの1つ目

        ```
        # kubectl logs -f my-nginx-xxxxxxxxxxxxxxxx
        ```

        `kubectl get pods -l run=my-nginx -o wide`に出力されたPodのNAMEの2つ目

        ```
        # kubectl logs -f my-nginx-yyyyyyyyyyyyyyyy
        ```

    1. 別のターミナルを2つ用意し、それぞれk8s-node1、k8s-node2 にログインしてmy-nginxにアクセスします。
       コマンドを何度か実行すると、ランダムにmy-nginx-xxxxxxxxxxxxxxxx,my-nginx-yyyyyyyyyyyyyyyyにアクセスされることがわかります。

        ```
        [root@k8s-node1 ~]# curl 192.168.123.151:30080
        <!DOCTYPE html>
        <html>
        <head>
        <title>Welcome to nginx!</title>
        ```

        ```
        [root@k8s-node2 ~]# curl 192.168.123.152:30080
        <!DOCTYPE html>
        <html>
        <head>
        <title>Welcome to nginx!</title>
        ：
        ```


1. 動作確認のために作成したService, Podの削除
    k8s-masterノードで以下のコマンドを実行します。

    ```
    # kubectl delete -f https://raw.githubusercontent.com/yasubehe/k8s-test001/master/sample-nginx-service-nodeport.yaml
    # kubectl delete -f https://raw.githubusercontent.com/yasubehe/k8s-test001/master/sample-nginx-pod.yaml
    ```


今後は、Kubernetesダッシュボードを動かしたりPrometheusでの監視を試してみようと思います。

#参考URL
[Bootstrapping clusters with kubeadm](https://kubernetes.io/docs/setup/production-environment/tools/kubeadm/)
[Kubernetes on CentOS on ESXi 環境を構築してみた](https://blog.yuu26.com/kubernetes-centos-esxi/)
[kubeadmでk8sクラスタを構築](https://qiita.com/murata-tomohide/items/be2171ac459f8f7d54f0)
[VirtualBox+CentOS 7+kubeadmを利用してKubernetesクラスタのローカル構築を試してみる](https://ops.jig-saw.com/tech-cate/virtualbox_centos_kubernetes)
[Install Calico networking and network policy for on-premises deployments](https://docs.projectcalico.org/getting-started/kubernetes/self-managed-onprem/onpremises)
[calicoが起動しない(CrashLoopBackOff)](https://qiita.com/Gin/items/e09e6800062a72839680#calico%E3%81%8C%E8%B5%B7%E5%8B%95%E3%81%97%E3%81%AA%E3%81%84crashloopbackoff)
[【Kubernetes】VirtualBox上のVM(CentOS 7)にkubeadmで1Master+2Node環境作ってみた](https://qiita.com/shinaK/items/a68b926be918f0daba24)