kubeadmでKubernetes HAクラスタを構築する(CentOS7、k8s v1.17)
以前、勉強のためにKubernetes v1.17でHAクラスタ(高可用性クラスタ)を構築した際のメモ。
現時点(2022年10月)のK8s最新バージョンはv1.25なので、やや古くなってしまっているが、大まかな手順は変わって無さそうなので記録として残しておく。
気が向いたらK8sやOSバージョンを新しいものにして再度構築してみたいと思っている。
構成の検討
構築するHAクラスタの構成を決める。
HAクラスタの構成にはオプションがあるので、まずは以下ページを見てみる。
高可用性トポロジーのためのオプション | Kubernetes
→etcdクラスターを、K8sのコントロールプレーンと同居させるかどうかで、2つのパターンがある。
今回は検証目的で、etcd専用のマシンを用意するのも大変なので、「積層コントロールプレーンノードを使用する方法」を選択した。HAクラスタの構築手順は、以下ページに載っている。
kubeadmを使用した高可用性クラスターの作成 | Kubernetes
→HAクラスタの場合は、kube-apiserver用ロードバランサーが必要とのこと。今回はNginxを使うことにした。
→通常のK8sクラスタと同様、SCNIが必要なので、何を使うか決めておく。
CNIは代表的なものとしてCalico、Flannelがあるが、FlannelはK8sの「Network Policy」機能が使えないので、Calicoを採用した。
※以下の理由からも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
※ちなみに、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の偏りの解消方法等)が必要。