tk_ch’s blog

インフラエンジニアのブログ

kubeadmでKubernetes HAクラスタを構築する(CentOS7、k8s v1.17)

以前、勉強のためにKubernetes v1.17でHAクラスタ(高可用性クラスタ)を構築した際のメモ。
現時点(2022年10月)のK8s最新バージョンはv1.25なので、やや古くなってしまっているが、大まかな手順は変わって無さそうなので記録として残しておく。
気が向いたらK8sやOSバージョンを新しいものにして再度構築してみたいと思っている。

構成の検討

構築するHAクラスタの構成を決める。

※以下の理由からもCalicoをオススメする。
kubeadmを使用したクラスターの作成 | Kubernetes
→2022年10月時点で、以下の記載がある。
 特別な理由がなければ、kubeadmでK8sインストールする場合はCalicoを選択するのがよさそう。

現在、Calicoはkubeadmプロジェクトがe2eテストを実施している唯一のCNIプラグインです。

構成図

前項での検討を踏まえて、構築する環境の構成イメージは以下の通り。

構築する環境の構成図
構築する環境の構成図

環境

  • OS:CentOS7
  • スペック:2vcpu、メモリ4GB、ディスクサイズは適当に30~50GBくらい。
  • ネットワーク:yumやdocker pullを実行するため、各ノードからインターネットに接続可能にしておく。
  • ホスト名、IPアドレスは以下の通り。
      ・LBノード#1:dev-klb-001(192.168.10.190)
      ・Masterノード#1:dev-k8s-001(192.168.10.191)
      ・Masterノード#2:dev-k8s-002(192.168.10.192)
      ・Masterノード#3:dev-k8s-003(192.168.10.193)
      ・Workerノード#1:dev-k8s-101(192.168.10.194)
      ・Workerノード#2:dev-k8s-102(192.168.10.195)
      ・Workerノード#3:dev-k8s-103(192.168.10.196)

→上記の7台の仮想マシンを、ローカルの環境に構築した。
※AWSnのEC2など、パブリッククラウドのIaaSで構築した方が手軽で良いかと思ったが、ServiceのLoadbalancerTypeが使用できないなどの制約(※以下ページ参照)があるため、ローカル環境に構築した。

kubeadmを使用した高可用性クラスターの作成 | Kubernetes

このページはクラウド上でクラスターを構築することには対応していません。ここで説明されているどちらのアプローチも、クラウド上で、LoadBalancerタイプのServiceオブジェクトや、動的なPersistentVolumeを利用して動かすことはできません。

構築作業

kubeadmのインストール

以下を参考に、kubeadmをインストールする。
kubeadmのインストール | Kubernetes
Masterノード、Workerノード全台で以下を実施する。

kubeadmのインストール要件を満たすように、OS設定の確認、変更を行う。

MACアドレスを表示し、他ノードと重複していないか確認する
# ip link show eth0 | grep link | cut -d " " -f 6

UUIDを表示し、他ノードと重複していないか確認する
# cat /sys/class/dmi/id/product_uuid

swapの無効化
# swapon -s
Filename                                Type            Size    Used    Priority
/dev/dm-1                               partition       2097148 0       -2
# swapoff -a
# swapon -s

swapの行をコメントアウトする
# vim /etc/fstab

SELinuxの無効化
# sed -i "s/SELINUX=enforcing/SELINUX=disabled/g" /etc/selinux/config
# reboot

Firewalldの無効化
# systemctl stop firewalld
# systemctl disable firewalld

K8sはIPフォワード機能を使うので、有効にする
# echo "net.ipv4.ip_forward = 1" >> /etc/sysctl.conf
# sysctl -p
net.ipv4.ip_forward = 1

Dockerをインストールする。

必要なパッケージのインストール
# yum install -y yum-utils device-mapper-persistent-data lvm2

dockerをインストール
# yum-config-manager --add-repo https://download.docker.com/linux/centos/docker-ce.repo
# yum update -y && yum install -y --setopt=obsoletes=0 docker-ce-19.03.8
# rpm -qa | grep docker | sort
docker-ce-19.03.8-3.el7.x86_64
docker-ce-cli-19.03.8-3.el7.x86_64

dockerを起動&自動起動設定
# systemctl enable docker && systemctl start docker
# docker version
Client: Docker Engine - Community
 Version:           19.03.8
 API version:       1.40
 Go version:        go1.12.17
 Git commit:        afacb8b
 Built:             Wed Mar 11 01:27:04 2020
 OS/Arch:           linux/amd64
 Experimental:      false

Server: Docker Engine - Community
 Engine:
  Version:          19.03.8
  API version:      1.40 (minimum version 1.12)
  Go version:       go1.12.17
  Git commit:       afacb8b
  Built:            Wed Mar 11 01:25:42 2020
  OS/Arch:          linux/amd64
  Experimental:     false
 containerd:
  Version:          1.2.13
  GitCommit:        7ad184331fa3e55e52b890ea95e65ba581ae3429
 runc:
  Version:          1.0.0-rc10
  GitCommit:        dc9208a3303feef5b3839f4323d9beb36df0a9dd
 docker-init:
  Version:          0.18.0
  GitCommit:        fec3683

dockerのcgrop driverがkubeletのデフォルト設定である「cgroupfs」であることを確認
# docker info | grep -i cgroup
WARNING: bridge-nf-call-iptables is disabled
WARNING: bridge-nf-call-ip6tables is disabled
 Cgroup Driver: cgroupfs

kubeadmをインストールする

yumレポジトリの追加
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

kubeadm・kubelet・kubectlインストール
# yum install -y kubelet kubeadm kubectl --disableexcludes=kubernetes

kubelet起動&自動起動設定
# systemctl enable --now kubelet

kubeletは正常に起動していない(activatingのまま)が、この時点ではこれが正常。
# systemctl status kubelet
● kubelet.service - kubelet: The Kubernetes Node Agent
   Loaded: loaded (/usr/lib/systemd/system/kubelet.service; enabled; vendor preset: disabled)
  Drop-In: /usr/lib/systemd/system/kubelet.service.d
           mq10-kubeadm.conf
   Active: activating (auto-restart) (Result: exit-code) since 月 2020-03-16 19:49:13 JST; 7s ago
     Docs: https://kubernetes.io/docs/
  Process: 9882 ExecStart=/usr/bin/kubelet $KUBELET_KUBECONFIG_ARGS $KUBELET_CONFIG_ARGS $KUBELET_KUBEADM_ARGS $KUBELET_EXTRA_ARGS (code=exited, status=255)
 Main PID: 9882 (code=exited, status=255)

 3月 16 19:49:13 dev-k8s-001 systemd[1]: kubelet.service: main process exited, code=exited, status=255/n/a
 3月 16 19:49:13 dev-k8s-001 systemd[1]: Unit kubelet.service entered failed state.
 3月 16 19:49:13 dev-k8s-001 systemd[1]: kubelet.service failed.

net.bridge.bridge-nf-call-iptablesの設定を実施
# cat <<EOF > /etc/sysctl.d/k8s.conf
net.bridge.bridge-nf-call-ip6tables = 1
net.bridge.bridge-nf-call-iptables = 1
EOF
# sysctl --system
# sysctl -n net.bridge.bridge-nf-call-iptables
1

br_netfilterモジュールが稼働していることを確認
# lsmod | grep br_netfilter
br_netfilter           22256  0
bridge                151336  1 br_netfilter

ノード間がホスト名で通信できるよう、各ノードのhostsファイルに全ノードのホスト名とIPアドレスを記載しておく。 (DNSサーバがある環境ならそれで名前解決すればOK。)

# vim /etc/hosts
127.0.0.1   localhost localhost.localdomain localhost4 localhost4.localdomain4
::1         localhost localhost.localdomain localhost6 localhost6.localdomain6
192.168.10.190 dev-klb-001
192.168.10.191 dev-k8s-001
192.168.10.192 dev-k8s-002
192.168.10.193 dev-k8s-003
192.168.10.194 dev-k8s-101
192.168.10.195 dev-k8s-102
192.168.10.196 dev-k8s-103

ロードバランサー(LBノード)の構築

ロードバランサーとしてNginxを使う。
6443ポートで受け付けたトラフィックを、Masterノード3台の6443ポートに転送する設定にする。

LBノードで以下のようにnginxを設定して起動しておく
# cat /etc/nginx/nginx.conf

user  nginx;
worker_processes  1;

error_log  /var/log/nginx/error.log warn;
pid        /var/run/nginx.pid;


events {
    worker_connections  1024;
}

stream {
  upstream kube_apiserver {
    least_conn;
    server 192.168.10.191:6443;
    server 192.168.10.192:6443;
    server 192.168.10.193:6443;
    }

  server {
    listen        6443;
    proxy_pass    kube_apiserver;
    proxy_timeout 10m;
    proxy_connect_timeout 1s;
  }
}

http {
    include       /etc/nginx/mime.types;
    default_type  application/octet-stream;

    log_format  main  '$remote_addr - $remote_user [$time_local] "$request" '
                      '$status $body_bytes_sent "$http_referer" '
                      '"$http_user_agent" "$http_x_forwarded_for"';

    access_log  /var/log/nginx/access.log  main;

    sendfile        on;
    #tcp_nopush     on;

    keepalive_timeout  65;

    #gzip  on;

#    include /etc/nginx/conf.d/*.conf;
}

Masterノードの構築

Masterノード#1で以下を実行する。

kubeadmの設定ファイルを書く。
certSANsやcontrolPlaneEndpointをMASTERノードでは無くLBノードにするのがポイント。
# vim /etc/kubernetes/kubeadm-config.yaml
apiVersion: kubeadm.k8s.io/v1beta1
kind: ClusterConfiguration
kubernetesVersion: v1.17.4
apiServer:
  certSANs:
  - dev-klb-001
networking:
  podSubnet: 10.64.0.0/16
controlPlaneEndpoint: dev-klb-001:6443

initを実行する。出力結果は後々使用するため控えておく。
# kubeadm init --config=/etc/kubernetes/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 dev-klb-001:6443 --token uqvtkv.clz9iyjobripct2w \
    --discovery-token-ca-cert-hash sha256:2d4d8d85ad6eeb3e08d349147b12202eb09dfe06797d31d44360b12c4b6377e0 \
    --control-plane

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

kubeadm join dev-klb-001:6443 --token uqvtkv.clz9iyjobripct2w \
    --discovery-token-ca-cert-hash sha256:2d4d8d85ad6eeb3e08d349147b12202eb09dfe06797d31d44360b12c4b6377e0

init実行時に表示されたコマンドを実行する
# mkdir -p $HOME/.kube
# sudo cp -i /etc/kubernetes/admin.conf $HOME/.kube/config
# sudo chown $(id -u):$(id -g) $HOME/.kube/config

kubectlが使えるようになる。この時点ではSTATUSはNotReadyで大丈夫。
# kubectl get node
NAME          STATUS     ROLES    AGE     VERSION
dev-k8s-001   NotReady   master   7m50s   v1.17.4

CNIとしてCalicoをデプロイする。まずyamlの以下2点を修正する。
・CALICO_IPV4POOL_IPIPをoffにする
・CALICO_IPV4POOL_CIDRのvalueはkubeadm-config.yamlのpodSubnetと一致させる。
# wget https://docs.projectcalico.org/manifests/calico.yaml
# vim calico.yaml
(略)
            # Enable IPIP
            - name: CALICO_IPV4POOL_IPIP
              value: "off"
            # Set MTU for tunnel device used if ipip is enabled
            - name: FELIX_IPINIPMTU
              valueFrom:
                configMapKeyRef:
                  name: calico-config
                  key: veth_mtu
            # The default IPv4 pool to create on startup if none exists. Pod IPs will be
            # chosen from this range. Changing this value after installation will have
            # no effect. This should fall within `--cluster-cidr`.
            - name: CALICO_IPV4POOL_CIDR
              value: "10.64.0.0/16"
(略)

Calicoを展開する。
# kubectl apply -f calico.yaml

Calicoのバージョンを確認
# grep image calico.yaml
          image: calico/cni:v3.13.2
          image: calico/cni:v3.13.2
          image: calico/pod2daemon-flexvol:v3.13.2
          image: calico/node:v3.13.2
          image: calico/kube-controllers:v3.13.2

PodのStatusが全てRunnnigになったらOK。
# kubectl get pod -n kube-system
NAME                                       READY   STATUS    RESTARTS   AGE
calico-kube-controllers-5554fcdcf9-zmdlw   1/1     Running   0          45s
calico-node-6c4zt                          1/1     Running   0          45s
coredns-6955765f44-frnbn                   1/1     Running   0          11m
coredns-6955765f44-r28fc                   1/1     Running   0          11m
etcd-dev-k8s-001                           1/1     Running   0          11m
kube-apiserver-dev-k8s-001                 1/1     Running   0          11m
kube-controller-manager-dev-k8s-001        1/1     Running   0          11m
kube-proxy-z5r6m                           1/1     Running   0          11m
kube-scheduler-dev-k8s-001                 1/1     Running   0          11m

K8sの証明書ファイルは全Masterノードで同じものを使うため、Masterノード#2、#3にコピーする。
# USER=root
# CONTROL_PLANE_IPS="dev-k8s-002 dev-k8s-003"
# 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

Masterノード#2、#3で以下を実行する

Masterノード#1から転送した証明書をK8sのディレクトリに配置する。
# mkdir -p /etc/kubernetes/pki/etcd
# mv /root/ca.crt /etc/kubernetes/pki/
# mv /root/ca.key /etc/kubernetes/pki/
# mv /root/sa.pub /etc/kubernetes/pki/
# mv /root/sa.key /etc/kubernetes/pki/
# mv /root/front-proxy-ca.crt /etc/kubernetes/pki/
# mv /root/front-proxy-ca.key /etc/kubernetes/pki/
# mv /root/etcd-ca.crt /etc/kubernetes/pki/etcd/ca.crt
# mv /root/etcd-ca.key /etc/kubernetes/pki/etcd/ca.key
# mv /root/admin.conf /etc/kubernetes/admin.conf

Masterノードとしてクラスタに参加する。
Masterノード#1でkubeadm initを実行したときに表示されたコマンドを実行する。
#   kubeadm join dev-klb-001:6443 --token uqvtkv.clz9iyjobripct2w \
    --discovery-token-ca-cert-hash sha256:2d4d8d85ad6eeb3e08d349147b12202eb09dfe06797d31d44360b12c4b6377e0 \
    --control-plane

Masterノードの状態を確認する

STATUSが3台ともReadyになっていればOK。
# kubectl get node
NAME          STATUS   ROLES    AGE   VERSION
dev-k8s-001   Ready    master   70m   v1.17.4
dev-k8s-002   Ready    master   17m   v1.17.4
dev-k8s-003   Ready    master   39m   v1.17.4

PodのStatusが全てRunnnigになったらOK。
# kubectl get pod -n kube-system
NAME                                       READY   STATUS    RESTARTS   AGE
calico-kube-controllers-5554fcdcf9-zmdlw   1/1     Running   0          18m
calico-node-5hwlw                          1/1     Running   0          6m14s
calico-node-6c4zt                          1/1     Running   0          18m
calico-node-f4d48                          1/1     Running   0          14m
coredns-6955765f44-frnbn                   1/1     Running   0          28m
coredns-6955765f44-r28fc                   1/1     Running   0          28m
etcd-dev-k8s-001                           1/1     Running   0          28m
etcd-dev-k8s-002                           1/1     Running   0          14m
etcd-dev-k8s-003                           1/1     Running   0          6m12s
kube-apiserver-dev-k8s-001                 1/1     Running   0          28m
kube-apiserver-dev-k8s-002                 1/1     Running   0          14m
kube-apiserver-dev-k8s-003                 1/1     Running   0          6m14s
kube-controller-manager-dev-k8s-001        1/1     Running   1          28m
kube-controller-manager-dev-k8s-002        1/1     Running   0          14m
kube-controller-manager-dev-k8s-003        1/1     Running   0          6m14s
kube-proxy-k26ls                           1/1     Running   0          6m14s
kube-proxy-rql7k                           1/1     Running   0          14m
kube-proxy-z5r6m                           1/1     Running   0          28m
kube-scheduler-dev-k8s-001                 1/1     Running   1          28m
kube-scheduler-dev-k8s-002                 1/1     Running   0          14m
kube-scheduler-dev-k8s-003                 1/1     Running   0          6m14s

kubectlのtabでのコマンド補完とエイリアスを設定しておく(全てのMasterノードでやっておく)。

コマンド補完とエイリアスの設定
# source <(kubectl completion bash)
# echo "source <(kubectl completion bash)" >> ~/.bashrc
# echo "alias k=kubectl" >> ~/.bashrc
# echo "complete -F __start_kubectl k" >> ~/.bashrc

「k」でkubectlを実行できる。tab補完も効く。
# k get node
NAME          STATUS   ROLES    AGE   VERSION
dev-k8s-001   Ready    master   70m   v1.17.4
dev-k8s-002   Ready    master   17m   v1.17.4
dev-k8s-003   Ready    master   39m   v1.17.4

[kubernetes.io

※ちなみに、kube-proxyをipvsモードで動作させたい場合は、kubeadm-config.yamlを以下のようにすればよい。

# cat /etc/kubernetes/kubeadm-config.yaml
apiVersion: kubeadm.k8s.io/v1beta1
kind: ClusterConfiguration
kubernetesVersion: v1.17.4
apiServer:
  certSANs:
  - dev-klb-001
networking:
  podSubnet: 10.64.0.0/16
controlPlaneEndpoint: dev-klb-001:6443
---
apiVersion: kubeproxy.config.k8s.io/v1alpha1
kind: KubeProxyConfiguration
mode: ipvs

Workerノードの構築

全Workerノードで以下を実行する。

Workerノードとしてクラスタに参加する。
Masterノード#1でkubeadm initを実行したときに表示されたコマンドを実行する。
# kubeadm join dev-klb-001:6443 --token uqvtkv.clz9iyjobripct2w \
    --discovery-token-ca-cert-hash sha256:2d4d8d85ad6eeb3e08d349147b12202eb09dfe06797d31d44360b12c4b6377e0
(略)
This node has joined the cluster:
* Certificate signing request was sent to apiserver and a response was received.
* The Kubelet was informed of the new secure connection details.

Run 'kubectl get nodes' on the control-plane to see this node join the cluster.

Masterノードで全ノードのSTATUSがReadyであることを確認する。

# k get node
NAME          STATUS   ROLES    AGE     VERSION
dev-k8s-001   Ready    master   37m     v1.17.4
dev-k8s-002   Ready    master   23m     v1.17.4
dev-k8s-003   Ready    master   15m     v1.17.4
dev-k8s-101   Ready    <none>   6m16s   v1.17.4
dev-k8s-102   Ready    <none>   3m6s    v1.17.4
dev-k8s-103   Ready    <none>   2m32s   v1.17.4

動作確認

構築できたので、まずはK8sクラスタとして正常に動作しているか確認する。

機能を色々試す

HAクラスタだとkube-apiserverへの接続がLBノード経由になっていることが気になったので、適当にPodを起動して、kube-apiserverに接続できるか確認してみる。

適当にPodを起動
# kubectl run testpod --image=centos:7 -i --tty --rm

PodからServiceの名前解決が出来ていることを確認する
[root@testpod-7f47d69745-k6jmh /]# nslookup kubernetes.default.svc.cluster.local
Server:         10.96.0.10
Address:        10.96.0.10#53

Name:   kubernetes.default.svc.cluster.local
Address: 10.96.0.1

Podからkube-apiserverへの接続ができているか確認する。
[root@testpod-7f47d69745-k6jmh /]# curl https://10.96.0.1:443/api?timeout=32s
curl: (60) Peer certificate cannot be authenticated with known CA certificates
More details here: http://curl.haxx.se/docs/sslcerts.html

curl performs SSL certificate verification by default, using a "bundle"
 of Certificate Authority (CA) public keys (CA certs). If the default
 bundle file isn't adequate, you can specify an alternate file
 using the --cacert option.
If this HTTPS server uses a certificate signed by a CA represented in
 the bundle, the certificate verification probably failed due to a
 problem with the certificate (it might be expired, or the name might
 not match the domain name in the URL).
If you'd like to turn off curl's verification of the certificate, use
 the -k (or --insecure) option.

→証明書を指定せずにアクセスしたので警告メッセージが出ているが、接続自体は成功している。

Pod間通信ができていることを確認する。

テスト用Deploymentを作成する。
# vim sample-deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: sample-deployment
spec:
  replicas: 3
  selector:
    matchLabels:
      app: sample-app
  template:
    metadata:
      labels:
        app: sample-app
    spec:
      containers:
        - name: nginx-container
          image: nginx:1.12
          ports:
            - containerPort: 80

# kubectl apply -f sample-deployment.yaml
deployment.apps/sample-deployment created

問題なく起動している。
# k get deployment
NAME                READY   UP-TO-DATE   AVAILABLE   AGE
sample-deployment   3/3     3            3           3m41s
# k get pod
NAME                                READY   STATUS    RESTARTS   AGE
sample-deployment-c6c6778b4-6l98t   1/1     Running   0          3m53s
sample-deployment-c6c6778b4-98vtp   1/1     Running   0          3m53s
sample-deployment-c6c6778b4-xlnzv   1/1     Running   0          3m53s

Pod内のindex.htmlの中身をホスト名に変更し、どのPodにアクセスしたか判別できるようにする。
# for PODNAME in `kubectl get pod -l app=sample-app -o jsonpath='{.items[*].metadata.name}'`; do kubectl exec -it ${PODNAME} -- cp /etc/hostname /usr/share/nginx/html/index.html; done

# for PODNAME in `kubectl get pod -l app=sample-app -o jsonpath='{.items[*].metadata.name}'`; do kubectl exec -it ${PODNAME} -- cat /usr/share/nginx/html/index.html; done
sample-deployment-c6c6778b4-6l98t
sample-deployment-c6c6778b4-98vtp
sample-deployment-c6c6778b4-xlnzv

ノードを跨いだPod間通信が出来ているか確認する。
Podは3台のWorkerノードそれぞれで起動している。
# k get pod -o wide
NAME                                READY   STATUS    RESTARTS   AGE     IP             NODE          NOMINATED NODE   READINESS GATES
sample-deployment-c6c6778b4-6l98t   1/1     Running   0          4m50s   10.64.85.64    dev-k8s-101   <none>           <none>
sample-deployment-c6c6778b4-98vtp   1/1     Running   0          4m50s   10.64.184.65   dev-k8s-102   <none>           <none>
sample-deployment-c6c6778b4-xlnzv   1/1     Running   0          4m50s   10.64.15.192   dev-k8s-103   <none>           <none>
    
テスト用Podを起動して、各Podにアクセスできることを確認。
# kubectl run testpod --image=centos:7 -i --tty --rm
[root@testpod-6fcc4dc99-5tq4f /]# curl http://10.64.85.64
sample-deployment-c6c6778b4-6l98t
[root@testpod-6fcc4dc99-5tq4f /]# curl http://10.64.184.65
sample-deployment-c6c6778b4-98vtp
[root@testpod-6fcc4dc99-5tq4f /]# curl http://10.64.15.192
sample-deployment-c6c6778b4-xlnzv

ClusterIPタイプのServiceを試してみる。

ClusterIPタイプのServiceを作成。
# cat sample-clusterip.yaml
apiVersion: v1
kind: Service
metadata:
  name: sample-clusterip
spec:
  type: ClusterIP
  ports:
    - name: "http-port"
      protocol: "TCP"
      port: 8080
      targetPort: 80
  selector:
    app: sample-app

# k apply -f sample-clusterip.yaml

# kubectl describe service sample-clusterip
Name:              sample-clusterip
Namespace:         default
Labels:            <none>
Annotations:       kubectl.kubernetes.io/last-applied-configuration:
                     {"apiVersion":"v1","kind":"Service","metadata":{"annotations":{},"name":"sample-clusterip","namespace":"default"},"spec":{"ports":[{"name"...
Selector:          app=sample-app
Type:              ClusterIP
IP:                10.109.85.22
Port:              http-port  8080/TCP
TargetPort:        80/TCP
Endpoints:         10.64.15.192:80,10.64.184.65:80,10.64.85.64:80
Session Affinity:  None
Events:            <none>

テスト用PodからClusterIPのエンドポイント(sample-clusterip:8080)に対してアクセスすると、配下の各Podに振り分けられていることが確認できる。
# kubectl run testpod --image=centos:7 -i --tty --rm
kubectl run --generator=deployment/apps.v1 is DEPRECATED and will be removed in a future version. Use kubectl run --generator=run-pod/v1 or kubectl create instead.
If you don't see a command prompt, try pressing enter.
[root@testpod-6fcc4dc99-qk8qb /]# curl -s http://sample-clusterip:8080/
sample-deployment-c6c6778b4-xlnzv
[root@testpod-6fcc4dc99-qk8qb /]# curl -s http://sample-clusterip:8080/
sample-deployment-c6c6778b4-98vtp
[root@testpod-6fcc4dc99-qk8qb /]# curl -s http://sample-clusterip:8080/
sample-deployment-c6c6778b4-6l98t

clusterIPを削除する。
# k delete -f sample-clusterip.yaml

NodePortタイプのServiceを試してみる。

NodePortタイプのServiceを作成する。
# cat sample-nodeport.yaml
apiVersion: v1
kind: Service
metadata:
  name: sample-nodeport
spec:
  type: NodePort
  ports:
    - name: "http-port"
      protocol: "TCP"
      port: 8080
      targetPort: 80
      nodePort: 30080
  selector:
    app: sample-app

#  kubectl apply -f sample-nodeport.yaml

# kubectl get svc -o wide
NAME              TYPE        CLUSTER-IP       EXTERNAL-IP   PORT(S)          AGE   SELECTOR
kubernetes        ClusterIP   10.96.0.1        <none>        443/TCP          57m   <none>
sample-nodeport   NodePort    10.109.230.138   <none>        8080:30080/TCP   5s    app=sample-app

# kubectl describe service sample-nodeport
Name:                     sample-nodeport
Namespace:                default
Labels:                   <none>
Annotations:              kubectl.kubernetes.io/last-applied-configuration:
                            {"apiVersion":"v1","kind":"Service","metadata":{"annotations":{},"name":"sample-nodeport","namespace":"default"},"spec":{"ports":[{"name":...
Selector:                 app=sample-app
Type:                     NodePort
IP:                       10.109.230.138
Port:                     http-port  8080/TCP
TargetPort:               80/TCP
NodePort:                 http-port  30080/TCP
Endpoints:                10.64.15.192:80,10.64.184.65:80,10.64.85.64:80
Session Affinity:         None
External Traffic Policy:  Cluster
Events:                   <none>

どのノードに対してアクセスしても、各Podに振り分けされる。
[root@dev-k8s-001 work]# curl http://dev-k8s-101:30080/
sample-deployment-c6c6778b4-6l98t
[root@dev-k8s-001 work]# curl http://dev-k8s-102:30080/
sample-deployment-c6c6778b4-98vtp
[root@dev-k8s-001 work]# curl http://dev-k8s-103:30080/
sample-deployment-c6c6778b4-98vtp
[root@dev-k8s-001 work]# curl http://dev-k8s-101:30080/
sample-deployment-c6c6778b4-98vtp
[root@dev-k8s-001 work]# curl http://dev-k8s-101:30080/
sample-deployment-c6c6778b4-6l98t
[root@dev-k8s-001 work]# curl http://dev-k8s-101:30080/
sample-deployment-c6c6778b4-xlnzv

NodePortを削除する。
# k delete -f sample-nodeport.yaml

テスト用Deploymentを削除する。

# kubectl delete -f sample-deployment.yaml
deployment.apps "sample-deployment" deleted

# k get deployment
No resources found in default namespace.

テストツール(sonobuoy)で評価する

KubernetesクラスタのE2Eテストを実行してくれるsonobuoyというツールがある。
これを実行して、クラスタの正常性を確認してみる。
sonobuoy.io

sonobuoyをダウンロードする
# wget https://github.com/vmware-tanzu/sonobuoy/releases/download/v0.18.0/sonobuoy_0.18.0_linux_amd64.tar.gz
# tar -xvf sonobuoy_0.18.0_linux_amd64.tar.gz

sonobuoyを開始する
# ./sonobuoy run

sonobupyのPodがクラスタ上に展開されたことが確認できる。
# k -n sonobuoy get pod
NAME                                                      READY   STATUS    RESTARTS   AGE
sonobuoy                                                  1/1     Running   0          3m58s
sonobuoy-e2e-job-3d8ef3ce1e3e4f34                         2/2     Running   0          3m46s
sonobuoy-systemd-logs-daemon-set-079b4675f756435f-2hv6p   2/2     Running   0          3m46s
sonobuoy-systemd-logs-daemon-set-079b4675f756435f-54m58   2/2     Running   0          3m45s
sonobuoy-systemd-logs-daemon-set-079b4675f756435f-fswt7   2/2     Running   0          3m45s
sonobuoy-systemd-logs-daemon-set-079b4675f756435f-k2g46   2/2     Running   0          3m46s
sonobuoy-systemd-logs-daemon-set-079b4675f756435f-pdc87   2/2     Running   0          3m46s
sonobuoy-systemd-logs-daemon-set-079b4675f756435f-xf6qq   2/2     Running   0          3m45s

statusコマンドで実行にかかる時間が表示できる。60分くらいかかる模様。
# ./sonobuoy status
         PLUGIN     STATUS   RESULT   COUNT
            e2e    running                1
   systemd-logs   complete                6

Sonobuoy is still running. Runs can take up to 60 minutes.

しばらく経ってstatusを実行すると完了している。
RESULTがpassedとなっているので問題なさそう。
# ./sonobuoy status
         PLUGIN     STATUS   RESULT   COUNT
            e2e   complete   passed       1
   systemd-logs   complete   passed       6

Sonobuoy has completed. Use `sonobuoy retrieve` to get results.

結果のサマリは以下のように取得できる。
# results=$(./sonobuoy retrieve)
# ./sonobuoy results $results
Plugin: e2e
Status: passed
Total: 4842
Passed: 278
Failed: 0
Skipped: 4564

Plugin: systemd-logs
Status: passed
Total: 6
Passed: 6
Failed: 0
Skipped: 0

結果の詳細レポートは以下のように取得できる。
# mkdir result
# tar zxf 202004090501_sonobuoy_81410fb8-12ef-4f6c-9150-3eb10d036ca9.tar.gz -C result/
# ll result/
合計 12
drwxr-xr-x 8 root root  120  4月  9 15:45 hosts
drwxr-xr-x 2 root root   80  4月  9 15:46 meta
drwxr-xr-x 4 root root   37  4月  9 15:45 plugins
drwxr-xr-x 3 root root   22  4月  9 15:45 podlogs
drwxr-xr-x 4 root root   31  4月  9 15:45 resources
-rw-r--r-- 1 root root 4501  4月  9 15:45 servergroups.json
-rw-r--r-- 1 root root  226  4月  9 15:45 serverversion.json

sonobouy関連リソースを削除する。
# ./sonobuoy delete --wait

sonobuoyにはE2Eテスト実行後に「Leaked End-to-end namespaces」という問題が発生する可能性があるため、念のため以下も実行する。

# ./sonobuoy delete --all

可用性の確認

Kubernetesクラスタとしては正常に動作していることが確認できた。
HAクラスタなので、Masterノードが1台ダウンしても動作することも確認したい。

Masterノード1台ダウン

事前準備として、前項で使ったDeployment、Service(NodePort)をクラスタに展開する。

Deployment、Pod、NodePortを以下のように作成した。
# kubectl get pods -o wide
NAME                                READY   STATUS    RESTARTS   AGE   IP             NODE          NOMINATED NODE   READINESS GATES
sample-deployment-c6c6778b4-dhvhd   1/1     Running   0          16s   10.64.85.99    dev-k8s-101   <none>           <none>
sample-deployment-c6c6778b4-j5blq   1/1     Running   0          16s   10.64.184.89   dev-k8s-102   <none>           <none>
sample-deployment-c6c6778b4-mwp6p   1/1     Running   0          16s   10.64.15.205   dev-k8s-103   <none>           <none>
# kubectl get deployment -o wide
NAME                READY   UP-TO-DATE   AVAILABLE   AGE   CONTAINERS        IMAGES       SELECTOR
sample-deployment   3/3     3            3           24s   nginx-container   nginx:1.12   app=sample-app
# kubectl get svc -o wide
NAME              TYPE        CLUSTER-IP     EXTERNAL-IP   PORT(S)          AGE     SELECTOR
kubernetes        ClusterIP   10.96.0.1      <none>        443/TCP          5h44m   <none>
sample-nodeport   NodePort    10.109.40.18   <none>        8080:30080/TCP   21s     app=sample-app

クラスタ外のサーバからNodePortに接続し続けておく。 (接続先はWorkerノードにする。)

#watch -n 2 "timeout 1 curl 192.168.10.194:30080"

この状態でMaster#1をシャットダウンし、NodePortへの接続に影響がないか、クラスタへのリソースのデプロイができるか確認してみる。

Masterノード#1(dev-k8s-001)をシャットダウンさせてみる。シャットダウンから1分以内に、dev-k8s-001がNotReadyになった。 また、NodePortへの接続には影響は無かった。

# k get node
NAME          STATUS     ROLES    AGE     VERSION
dev-k8s-001   NotReady   master   6h18m   v1.17.4
dev-k8s-002   Ready      master   6h3m    v1.17.4
dev-k8s-003   Ready      master   5h55m   v1.17.4
dev-k8s-101   Ready      <none>   5h46m   v1.17.4
dev-k8s-102   Ready      <none>   5h43m   v1.17.4
dev-k8s-103   Ready      <none>   5h42m   v1.17.4

kube-system namespaceにあるPodを確認すると、Masterノード#1で動作していたcorednsとcalico-controllerは別のノードに移動していることが分かる。
kube-apiserverとetcdはダウンを検知できておらず、STATUSがRunningのまま。
これが正しい挙動なのかは情報が無く分からなかった。

# k get pod -n kube-system -o wide
NAME                                       READY   STATUS        RESTARTS   AGE     IP               NODE          NOMINATED NODE   READINESS GATES
calico-kube-controllers-5554fcdcf9-8zrj4   1/1     Running       0          104s    10.64.85.105     dev-k8s-101   <none>           <none>
calico-kube-controllers-5554fcdcf9-zmdlw   1/1     Terminating   0          6h13m   10.64.68.2       dev-k8s-001   <none>           <none>
calico-node-5hwlw                          1/1     Running       0          6h1m    192.168.10.193   dev-k8s-003   <none>           <none>
calico-node-6c4zt                          1/1     Running       0          6h13m   192.168.10.191   dev-k8s-001   <none>           <none>
calico-node-f4d48                          1/1     Running       0          6h9m    192.168.10.192   dev-k8s-002   <none>           <none>
calico-node-r4krr                          1/1     Running       0          5h52m   192.168.10.194   dev-k8s-101   <none>           <none>
calico-node-vncqb                          1/1     Running       1          5h49m   192.168.10.195   dev-k8s-102   <none>           <none>
calico-node-zzdwp                          1/1     Running       0          5h49m   192.168.10.196   dev-k8s-103   <none>           <none>
coredns-6955765f44-frnbn                   1/1     Terminating   0          6h24m   10.64.68.1       dev-k8s-001   <none>           <none>
coredns-6955765f44-gzflk                   1/1     Running       0          104s    10.64.15.210     dev-k8s-103   <none>           <none>
coredns-6955765f44-psw2r                   1/1     Running       0          104s    10.64.184.96     dev-k8s-102   <none>           <none>
coredns-6955765f44-r28fc                   1/1     Terminating   0          6h24m   10.64.68.0       dev-k8s-001   <none>           <none>
etcd-dev-k8s-001                           1/1     Running       0          6h24m   192.168.10.191   dev-k8s-001   <none>           <none>
etcd-dev-k8s-002                           1/1     Running       0          6h9m    192.168.10.192   dev-k8s-002   <none>           <none>
etcd-dev-k8s-003                           1/1     Running       0          6h1m    192.168.10.193   dev-k8s-003   <none>           <none>
kube-apiserver-dev-k8s-001                 1/1     Running       0          6h24m   192.168.10.191   dev-k8s-001   <none>           <none>
kube-apiserver-dev-k8s-002                 1/1     Running       0          6h9m    192.168.10.192   dev-k8s-002   <none>           <none>
kube-apiserver-dev-k8s-003                 1/1     Running       0          6h1m    192.168.10.193   dev-k8s-003   <none>           <none>
kube-controller-manager-dev-k8s-001        1/1     Running       1          6h24m   192.168.10.191   dev-k8s-001   <none>           <none>
kube-controller-manager-dev-k8s-002        1/1     Running       0          6h9m    192.168.10.192   dev-k8s-002   <none>           <none>
kube-controller-manager-dev-k8s-003        1/1     Running       0          6h1m    192.168.10.193   dev-k8s-003   <none>           <none>
kube-proxy-bt8zk                           1/1     Running       1          5h49m   192.168.10.195   dev-k8s-102   <none>           <none>
kube-proxy-dhxzg                           1/1     Running       0          5h52m   192.168.10.194   dev-k8s-101   <none>           <none>
kube-proxy-k26ls                           1/1     Running       0          6h1m    192.168.10.193   dev-k8s-003   <none>           <none>
kube-proxy-q5k4l                           1/1     Running       0          5h49m   192.168.10.196   dev-k8s-103   <none>           <none>
kube-proxy-rql7k                           1/1     Running       0          6h9m    192.168.10.192   dev-k8s-002   <none>           <none>
kube-proxy-z5r6m                           1/1     Running       0          6h24m   192.168.10.191   dev-k8s-001   <none>           <none>
kube-scheduler-dev-k8s-001                 1/1     Running       1          6h24m   192.168.10.191   dev-k8s-001   <none>           <none>
kube-scheduler-dev-k8s-002                 1/1     Running       0          6h9m    192.168.10.192   dev-k8s-002   <none>           <none>
kube-scheduler-dev-k8s-003                 1/1     Running       0          6h1m    192.168.10.193   dev-k8s-003   <none>           <none>

この状態でDeploymentやServiceを作成して接続確認してみたが、特に問題なく実行できた。
(kube-apiserverとetcdは他のMasterノードでも動作しているので、Masterノード#1から移動していないくも、問題ないのかもしれない。)
また、Masterノード#1を起動すると自動的にSTATUSがReadyとなった。
ただし、別ノード移動したPod(corednsとcalico-controller)は自動的にMasterノード#1に戻って来ることは無いため、再配置したい場合はrollout等を実行する必要がある。
Masterノードが1台ダウンしても、Kubernetesクラスタとしては継続して使用できることが確認できた。

※corednsとcalico-controllerはMasterノード上で稼働すべきだと思うが、上記ではMasterノードダウン時にWorkerノードへ移動してしまっている。
 affinityやtolerationを使用して、Masterノード上にしか移動されないような制御が必要だと思う。

※上記では、ノードダウンからPod移動までに約5分かかった。
これは、v1.13.0からTaintBasedEvictionというPod退避機能がデフォルトで有効になったため。
tolerationSecondsがデフォルトでは300秒なので、Pod移動開始までに5分かかる。
tolerationSecondsを短くすれば、移動開始までの待ち時間を短縮することができるらしい。
https://kubernetes.io/docs/concepts/configuration/taint-and-toleration/#taint-based-evictions

Masterノード2台ダウン

HAクラスタはRaftアルゴリズムで実現しているため、3台クラスタの場合はMasterは1台ダウンまでは許容できる。 2台ダウンした場合どうなるか試したところ、以下の挙動となった。

  • Masterノードが2台ダウンしても、起動済みのPodの稼働とNodePortへの接続には影響は無かった。
  • ただし、Podの移動や状態の取得、Pod障害時の自動復旧など、コントロールプレーンへのアクセスが必要なアクションは全て動作しなかった。

テストツール(sonobuoy)で評価する(※上手くいかない)

上記だけでHAクラスタとして正常に動作していると言って良いのか不安だったため、Sonobuoyを使うことを考えた。
Masterノードが1台ダウンした状態でsonobuoyを実行し、パスできればよいのではないかと思ったが、結果的には上手くいかなかった。
理由は、ダウンしているノードがあるとそもそもsonobuoyの実行に失敗してしまうため。

Masterノード1台ダウン上程でsonobuoyを実行したところ、以下のようにRESULTがfailedになる。

# ./sonobuoy status
         PLUGIN     STATUS   RESULT   COUNT
            e2e   complete   failed       1
   systemd-logs   complete   passed       5
   systemd-logs     failed   failed       1

Sonobuoy has completed. Use `sonobuoy retrieve` to get results.

結果を見ると、そもそもe2eテスト自体に失敗しているように見える。

# results=$(./sonobuoy retrieve)
# ./sonobuoy results $results
Plugin: e2e
Status: failed
Total: 1
Passed: 0
Failed: 1
Skipped: 0

Failed tests:
BeforeSuite

Plugin: systemd-logs
Status: failed
Total: 6
Passed: 5
Failed: 1

詳細な結果レポートを見てみると、ダウンしているノードがあることが原因の模様。

# less plugins/e2e/results/global/e2e.log
(略)
Apr 10 09:17:57.193: INFO: Condition Ready of node dev-k8s-001 is false, but Node is tainted by NodeController with [{node-role.kubernetes.io/master  NoSchedule <nil>} {node.kubernetes.io/unreachable  NoSchedule 2020-04-10 09:11:59 +0000 UTC} {node.kubernetes.io/unreachable  NoExecute 2020-04-10 09:12:04 +0000 UTC}]. Failure
(略)
Apr 10 09:47:57.529: FAIL: Unexpected error:
    <*errors.errorString | 0xc00005b970>: {
        s: "timed out waiting for the condition",
    }
    timed out waiting for the condition
occurred
Failure [1800.654 seconds]
(略)

まとめ

  • kubeadmでKubernetes HAクラスタを構築できた。 また、Masterノードが3台中1台ダウンしても、K8sクラスタとして正常に使用できることを確認した。
  • 今回は検証のため、LBノードがSPOFになっている。
    本番環境で使うためにはLBノードの冗長化や、その他もろもろの検討(ノード障害後のPodの偏りの解消方法等)が必要。