kube-vipを用いたHA Kubernetes Clusterの構築

f:id:tenzen_hgst:20200609030552p:plain

はじめに

去年の記事でシングルMaster構成のKubernetes(以下K8s) Clusterを作成しました. tenzen.hatenablog.com

シングルマスター構成は細かい事を気にせず構築できて便利なのですが,実際にサービスとして稼働させる際はMasterの冗長化も行いたいところです.

そこで今回はHigh Availability(HA)構成のK8s Clusterを構築し, その上にDeepLearning環境を構築していきます. Qiitaやブログをみると,KeepAlivedとHAProxyをK8s Cluster外の別ノードで構築し,Masterの役割をロードバランシングしている例が拝見されます.しかし今回は以下のドキュメントに準拠しつつ,kube-vip(Kube-vip)を用いてMasterを冗長化していきます.

kubernetes.io

ここでK8sの役割名についてですが,公式ドキュメントに準拠し,本節以降HA構成をとったK8s Clusterにおいてetcdやapi-serverなどが載っているNodeをcontrol plane node,それ以外をnodeと呼びます.

また,以前の記事「Kubernetes(k8s) v1.16とNvidia-Docker2を用いたマルチノードDeepLearning環境の構築 - てんぜんの生存日誌」や「Rook-Cephfs + Nvidia Driver vs てんぜん - てんぜんの生存日誌」と 同じ作業部分も記述しているので,わかっているorもうやっている人が読みとばせるように一つ目の記事と同じ部分は節名に「(以前と同様#1)」,二つ目の記事と同じ部分は説明に「(以前と同様#2)」と記述しているので活用してください.

※別Nodeでetcd Clusterを構築する構成もありますが,本記事ではetcdをcontrol plane nodeに載せた構成を構築します.

Kubernetes Clusterの構築

今回構築するK8s環境の物理構成は以下の通りになります.本節以降はNode名が頻繁に出てきますので,適宜参照してください. また,今回は記事執筆時点で最新版のK8s,v1.18.3を使用します.

ホスト名 役割 CPU メモリ GPU OS
k8s-m0 control plane + node intel Core i5 9600K 64GB(Non ECC) なし Ubuntu 16.04LTS(linux-image-4.15.0-72-generic)
k8s-m1 control plane + node intel Core i5 9600K 64GB(Non ECC) なし Ubuntu 16.04LTS(linux-image-4.15.0-72-generic)
michiru control plane + node intelXeon Gold 6230 ×2 192GB(ECC Registerd) Quadro GP100 ×4 Ubuntu 16.04LTS(linux-image-4.15.0-72-generic)
utaha node intel Core i9 9900K 128GB(Non ECC) RTX2080Ti×2 Ubuntu 16.04LTS(linux-image-4.15.0-72-generic)
eriri node intel Core i9 9900K 128GB(Non ECC) RTX2080Ti×2 Ubuntu 16.04LTS(linux-image-4.15.0-72-generic)

Step01[全Node共通事項]準備

この章では各種ソフトウェアをインストールする前の準備を行います. 明記しない限り全てのnode,control plane nodeで同じ設定を行ってください.

linux カーネルバージョンの変更(以前と同様#2)

以下の記事で説明した経緯でlinux kernelのバージョンをあげます.(ubuntu 18.04など使用する場合は関係ありません.) tenzen.hatenablog.com

まず現在のKernel Imageを調べておきます.

$ sudo uname -a
Linux tenzen 4.4.0-170-generic #199-Ubuntu SMP Thu Nov 14 01:45:04 UTC 2019 x86_64 x86_64 x86_64 GNU/Linux

次に現在使用できるKernel Imageを探してきます.

$ sudo apt-cache search linux-image | grep image-4 | grep generic
(省略)
linux-image-4.15.0-54-generic - Signed kernel image generic
linux-image-4.15.0-55-generic - Signed kernel image generic
linux-image-4.15.0-58-generic - Signed kernel image generic
linux-image-4.15.0-60-generic - Signed kernel image generic
linux-image-4.15.0-62-generic - Signed kernel image generic
linux-image-4.15.0-64-generic - Signed kernel image generic
linux-image-4.15.0-65-generic - Signed kernel image generic
linux-image-4.15.0-66-generic - Signed kernel image generic
linux-image-4.15.0-69-generic - Signed kernel image generic
linux-image-4.15.0-70-generic - Signed kernel image generic
linux-image-4.15.0-72-generic - Signed kernel image generic
linux-image-4.15.0-74-generic - Signed kernel image generic
linux-image-4.15.0-76-generic - Signed kernel image generic
linux-image-4.15.0-88-generic - Signed kernel image generic
linux-image-4.15.0-91-generic - Signed kernel image generic
linux-image-4.15.0-96-generic - Signed kernel image generic
linux-image-4.15.0-99-generic - Signed kernel image generic
linux-image-4.4.0-101-generic - Linux kernel image for version 4.4.0 on 64 bit x86 SMP
linux-image-4.4.0-103-generic - Linux kernel image for version 4.4.0 on 64 bit x86 SMP
...(省略)

今回はlinux-image-4.15.0-72-genericを使用していきます

$ sudo apt-get install -y linux-image-4.15.0-72-generic
$ reboot

再起動すると一番新しいKernel Imageを選択するようになっているので切り替わっていると思います.

$ sudo uname -a
Linux k8s-m 4.15.0-72-generic #81~16.04.1-Ubuntu SMP Tue Nov 26 16:34:21 UTC 2019 x86_64 x86_64 x86_64 GNU/Linux

スワップ機能のオフ(以前と同様#1)

K8sは,スワップ機能に対応していないためスワップ機能をオフにする必要があります.

そこで以下のコマンドを実行してスワップ機能のオンオフを確認してください.

$ cat /etc/fstab

# /etc/fstab: static file system information.
#
# Use 'blkid' to print the universally unique identifier for a
# device; this may be used with UUID= as a more robust way to name devices
# that works even if disks are added and removed. See fstab(5).
#
# <file system> <mount point>   <type>  <options>       <dump>  <pass>
# / was on /dev/sda2 during installation
UUID=hoge hoge hoge /               ext4    errors=remount-ro 0       1
# /boot/efi was on /dev/sda1 during installation
UUID=hoge hoge  /boot/efi       vfat    umask=0077      0       1
swap was on /dev/sda3 during installation
#UUID=hoge hoge hoge none            swap    sw              0       0

上記のように「swap was on ~」の部分がコメントアウトされていない場合は,スワップ機能がオンになっているためコメントアウトしてスワップ機能をオフにしてください.その後以下のコマンドを実行してスワップがオフになっているか確認してください.

$ swapon -s

何か表示された場合は以下のコマンドを実行してスワップを削除します.

$ swapoff -a

ポートの解放

次に,kubernetesが使用するポートを開放していきます.

開放しなければならないポートはKubernetesの公式インストールガイドにも書いてあります.

kubernetes.io kubernetes.io

今回はufwを用いてポート制御を行うためufwをオンにし,確認を行ってください.

$ sudo ufw enable
$ sudo ufw status
Status: active

次に,Kubernetes公式インストールガイドにしたがって下記のポートを開けていきます.

また今回はflannelを用いて内部ネットワークを構築するため,以下のように他のポートも開けていきます.他にもkube-vipで通過うポートなど以下ポート番号表にあるポートを解放します.

kubernetes.io

Node ポート番号
control plane node 53, 6443-6444, 2379-2380, 8285, 8472, 9153, 10000, 10250-10252
node 53, 8285, 8472, 9153, 10250, 30000-32767

controls plane node

$ sudo ufw allow 53 &&\
sudo ufw allow 6443-6444 &&\
sudo ufw allow 2379:2380/tcp &&\
sudo ufw allow 8285 &&\
sudo ufw allow 8472 &&\
sudo ufw allow 9153 &&\
sudo ufw allow 10000 &&\
sudo ufw allow 10250:10252/tcp

node

$ sudo ufw allow 53 &&\
sudo ufw allow 6444 &&\
sudo ufw allow 8285 &&\
sudo ufw allow 8472 &&\
sudo ufw allow 9153 &&\
sudo ufw allow 10000 &&\
sudo ufw allow 10250 &&\
sudo ufw allow 30000:32767/tcp

name serverの変更(以前と同様)

最後にdnsmasqが悪さをしてgithubのissuesのようにKubernetesのCoreDNSを壊したり, github.com

コンテナ内から通信できなくなったりするためname serverを変更します.

まず,以下のコマンドを実行して設定変更の必要があるかどうかを確認します.

$ cat /etc/resolv.conf 
# Dynamic resolv.conf(5) file for glibc resolver(3) generated by resolvconf(8)
#     DO NOT EDIT THIS FILE BY HAND -- YOUR CHANGES WILL BE OVERWRITTEN
nameserver 127.0.1.1

上記のようにnameserverが「127.0.1.1」になっている場合は設定変更の必要があります.それ以外の場合は以下の作業を行わなくても大丈夫なはずです.

まず下記のコマンドでNetworkManager.confを表示させ,「dns=dnsmasq」の部分がコメントアウトになっているか確認してください. コメントアウトされていない場合は「dns=dnsmasq」の部分をコメントアウトしてください.

その後ネットワークマネージャを再起動し,resolve.confを再確認してnameserverが変わっていることを確認してください.その際127.0.1.1の設定が残っている場合コメントアウトするか,削除してください.

$ cat /etc/NetworkManager/NetworkManager.conf 
[main]
plugins=ifupdown,keyfile,ofono
dns=dnsmasq

[ifupdown]
managed=false
$ sudo systemctl restart network-manager
$ cat /etc/resolv.conf 
# Dynamic resolv.conf(5) file for glibc resolver(3) generated by resolvconf(8)
#     DO NOT EDIT THIS FILE BY HAND -- YOUR CHANGES WILL BE OVERWRITTEN
nameserver xxx.xxx.xxx.xxx

以上で準備は完了です.

Step02[全Node共通事項]:ソフトウェアのインストール

この章では各種ソフトウェアのインストールを行います. 明記しない限り全てのcontrol plane node,nodeで同じ設定を行ってください.

Nvidia Driver(以前と同様#2)

今回は最新のNvidia Driverの中でも比較的安定しているnvidia-440をインストールしました.

Nvidia Driverはcontrol plane nodeにのみインストールしてください.(control plane nodeにもGPUを積んでnodeとして使用する場合はcontrol plane nodeにもインストールしてください.)

まずはじめにNvidia DriverのインストーラLinux Headerを見てインストールするものを判断しているため,使用中のLinux Imageに該当するLinux Headerをインストールします.

$ sudo apt-get install -y linux-headers-4.15.0-72-generic
$ reboot

再起動後「/usr/src/」にlinux-headers-4.15.0-72-genericがあることを確認します.これがないとNvidia Driverを正常にインストールすることができませんのでよく確認しておいてください.

$ ls /usr/src
linux-headers-4.15.0-72-generic
...(省略)

Nvidia公式サイトから自分が使用中のGPUなどを選択してインストーラをダウンロードてきます.今回は440番を使用しました.

ダウンロードできたらダウンロードしてきたファイルの権限を変更後ディスプレイマネージャを切ってCUIモードでログインし直してください.(UbuntuならCtr+Alt+F1)

$ mv /home/tenzen/Downloads/NVIDIA-Linux-x86_64-440.44.run /home/tenzen
$ chmod 777 NVIDIA-Linux-x86_64-440.44.run
$ sudo service lightdm stop

CUIモードでログインできたらドライバをインストールしていきます.インストーラを起動すると青い画面になって色々聞かれるので使用しているホストに合わせて答えていってください.

$ sudo NVIDIA-Linux-x86_64-440.44.run

Docker(以前と同様#1)

次にDocker18.06.3をインストールします. 以下のコマンドを順番に実行していけばインストールできます. 途中,「sudo apt-cache policy docker-ce」コマンドの出力結果に「docker-ce=18.06.3~ce~3-0~ubuntu」があることを確認してください.

$ sudo apt-get install apt-transport-https ca-certificates curl software-properties-common
$ curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo apt-key add -
$ sudo apt-key fingerprint 0EBFCD88
$ sudo add-apt-repository \
   "deb [arch=amd64] https://download.docker.com/linux/ubuntu \
   $(lsb_release -cs) \
   stable"
$ sudo apt-get update
$ sudo apt-cache policy docker-ce
$ sudo apt-get install -y docker-ce=18.06.3~ce~3-0~ubuntu

ここで,指定バージョンのdockerがインストールできたかの確認をしてPC起動時にdockerが起動するよう設定しておきます.

$ docker -v
Docker version 18.06.3-ce, build d7080c1
$ service docker start && service docker status

Nvidia-Docker2

さらにNvidia-Docker2のインストールをしていきます.

Nvidia-Docker2はcontrol plane nodeにのみインストールしてください.(control plane nodeにもGPUを積んでいる場合はcontrol plane nodeにもインストールしてください.)

よく似た名前でNvidia-Dockerと言うものがありますが,別物なので間違ってインストールしないように気をつけてください. また,docker v19.03以降からdockerがgpuにネイティブ対応したためNvidia-Docker2ではなくnvidia-container-toolkitを使用するようになりました. しかしながら公式ドキュメント GitHub - NVIDIA/k8s-device-plugin: NVIDIA device plugin for Kubernetes にあるように記事執筆時点でnvidia-container-toolkitには対応しておらずNvidia-docker2を使用するようにアナウンスされているので注意してください.

Note that you need to install the nvidia-docker2 package and not the nvidia-container-toolkit. This is because the new --gpus options hasn't reached kubernetes yet.

以下のコマンドを順番に実行していきます.

$ curl -s -L https://nvidia.github.io/nvidia-docker/gpgkey | sudo apt-key add -
$ curl -s -L https://nvidia.github.io/nvidia-docker/ubuntu16.04/amd64/nvidia-docker.list | sudo tee /etc/apt/sources.list.d/nvidia-docker.list
$ sudo apt-get update
$ sudo apt-get install -y nvidia-docker2
$ sudo pkill -SIGHUP dockerd

これでNvidia-docker2のインストールは完了しました.

インストールが完了したら,nvidia-docker2のバージョン確認とdockerのランタイムをnvidia-docker2のものへ変更します.

まず,以下のコマンドでnvidia-docker2のバージョンを確認し,nvidia-dockerではなくnvidia-docker2がインストールできているか確認します.

$ sudo nvidia-docker version
NVIDIA Docker: 2.2.2
Client:
 Version:           18.06.3-ce
 API version:       1.38
 Go version:        go1.10.3
 Git commit:        d7080c1
 Built:             Wed Feb 20 02:27:18 2019
 OS/Arch:           linux/amd64
 Experimental:      false

Server:
 Engine:
  Version:          18.06.3-ce
  API version:      1.38 (minimum version 1.12)
  Go version:       go1.10.3
  Git commit:       d7080c1
  Built:            Wed Feb 20 02:26:20 2019
  OS/Arch:          linux/amd64
  Experimental:     false

次にdockerのランタイムをnvidiaのものへ変更をしていきます.

下記のコマンドを実行してnvidiaランタイムをダウンロード後,/etc/docker/daemon.jsonnvidiaランタイムに変更します.

$ sudo apt-get install nvidia-container-runtime
$ sudo tee /etc/docker/daemon.json <<EOF
{
    "default-runtime": "nvidia",
    "runtimes": {
        "nvidia": {
            "path": "/usr/bin/nvidia-container-runtime",
            "runtimeArgs": []
        }
    }
}
EOF
$ sudo pkill -SIGHUP dockerd
$ sudo systemctl daemon-reload
$ sudo systemctl restart docker

最後にnvidiaのランタイムが正しく読み込まれているかの確認を行います. 下記コマンドを実行後,GPUの情報が表示されれば正しく読み込まれています.

$ sudo docker run --rm nvidia/cuda nvidia-smi
Sun Jun  7 20:08:01 2020       
+-----------------------------------------------------------------------------+
| NVIDIA-SMI 440.44       Driver Version: 440.44       CUDA Version: 10.2     |
|-------------------------------+----------------------+----------------------+
| GPU  Name        Persistence-M| Bus-Id        Disp.A | Volatile Uncorr. ECC |
| Fan  Temp  Perf  Pwr:Usage/Cap|         Memory-Usage | GPU-Util  Compute M. |
|===============================+======================+======================|
|   0  Quadro GP100        Off  | 00000000:18:00.0 Off |                  Off |
| 45%   62C    P0    96W / 235W |    959MiB / 16276MiB |    100%      Default |
+-------------------------------+----------------------+----------------------+
|   1  Quadro GP100        Off  | 00000000:3B:00.0 Off |                  Off |
| 46%   64C    P0   163W / 235W |    643MiB / 16278MiB |    100%      Default |
+-------------------------------+----------------------+----------------------+
|   2  Quadro GP100        Off  | 00000000:5E:00.0 Off |                  Off |
| 49%   68C    P0   197W / 235W |    643MiB / 16278MiB |    100%      Default |
+-------------------------------+----------------------+----------------------+
|   3  Quadro GP100        Off  | 00000000:86:00.0 Off |                  Off |
| 48%   66C    P0   171W / 235W |    643MiB / 16278MiB |     96%      Default |
+-------------------------------+----------------------+----------------------+
                                                                               
+-----------------------------------------------------------------------------+
| Processes:                                                       GPU Memory |
|  GPU       PID   Type   Process name                             Usage      |
|=============================================================================|
+-----------------------------------------------------------------------------+

Kubernetes(以前と同様#1)

最後にKubernetesをインストールしていきます.何もツールを用いずインストールするを行うのは非常に難易度が高いため,今回はKubernetes公式もオススメしているkubeadmを用いてインストールを行っていきます.

kubernetes.io

以下のコマンドを実行してGoogleリポジトリの登録を行い,その後パッケージのインストールを行います.

$ sudo apt-get update
$ sudo apt-get update && sudo apt-get install -y apt-transport-https
$ curl -s https://packages.cloud.google.com/apt/doc/apt-key.gpg | sudo apt-key add -
$ echo deb http://apt.kubernetes.io/ kubernetes-xenial main | sudo tee -a /etc/apt/sources.list.d/kubernetes.list > /dev/null
$ sudo apt-get update
$ sudo apt-get install -y kubectl kubelet kubeadm

Step03:1台目のcontrol plane nodeのセットアップ

はじめにで述べたようにkube-vipを用いてHA構成の設定をした後, Step01でスワップ機能をオフにしましたが,念のためにもう一度オフにしておきます. その後,kubeadmを用いてセットアップしていきます. また,一台目のcontrol plane nodeとそれ以外のcontrol plane nodeでは設定が少し違う事に注意してください.

kube-vip

まずはじめにkube-vipについて軽く紹介した後に,実際の構築を行なっていきたいと思います.

kube-vipとは

kube-vipとは,以下アーキテクチャ図のようにK8s clusterのcontrol plane node内にデプロイされる仮想IPの管理とLoad Balancingの機能を兼ね備えた静的Podの事です.旧来のHA構成においてKeepAlivedが行なっていた仮想IPの管理と,HAProxyが行なっていたLoadBalancingを一つにまとめたようなコンポーネントになっております.

architecture_kube-vip
kube-vipのアーキテクチャ(引用元:https://kube-vip.io/architecture/)

kube-vipの設定

まず以下のようなマニフェストを「config.yaml」を「/etc/kube-vip/」に作成してください. 注意点として以下の設定はcontrol plane node3台を想定したマニフェストとなっているためそれ以外の構成の場合は適宜変更してください.(control plane nodeは3台以上用意しないと障害耐性がない.)

remotePeers:
- id: ${ID_NOT_LOCAL_NODE}
  address: ${IP_ADDR_NOT_LOCAL_NODE}
  port: 10000
- id: ${ID_NOT_LOCAL_NODE}
  address: ${IP_ADDR_NOT_LOCAL_NODE}
  port: 10000
localPeer:
  ${ID_LOCAL_NODE}
  address: ${IP_ADDR_LOCAL_NODE}
  port: 10000
vip: ${VI_IP_ADDR}
gratuitousARP: true
singleNode: false
startAsLeader: true
interface: ${NETWORK_INTERFACE_NAME}
loadBalancers:
- name: Kubernetes Control Plane
  type: tcp
  port: 6443
  bindToVip: true
  backends:
  - port: 6444
    address: ${IP_ADDR_CONTROL_PLANE_NODE}
  - port: 6444
    address: ${IP_ADDR_CONTROL_PLANE_NODE}
  - port: 6444
    address: ${IP_ADDR_CONTROL_PLANE_NODE}

また,以下マニフェスト内で「${hogehoge}」になっている部分は以下の表を参考に使用する環境に合わせて設定してください.

変数名 説明
ID_NOT_LOCAL_NODE control plane nodeのうち1台目以外のID*1
ID_NOT_LOCAL_NODE control plane nodeのうち1台目のID*1
IP_ADDR_NOT_LOCAL_NODE control plane nodeのうち1台目以外のIPアドレス
IP_ADDR_LOCAL_NODE control plane nodeのうち1台目以外のIPアドレス
VI_IP_ADDR kube-apiサーバのエンドポイントとして使用する仮想IPアドレス*2
NETWORK_INTERFACE_NAME 使用するネットワークインターフェース名(例:eno0)
P_ADDR_CONTROL_PLANE_NODE control plane nodeのIPアドレス

1 一意であればなんでもOK 2 必ず使用していないIPアドレスを使用しなければならない.

マニフェストが完成したら以下のdocker コマンドを実行して設定を反映させます.

$ docker run -it --rm plndr/kube-vip:0.1.1 /kube-vip sample manifest \
    | sed "s|plndr/kube-vip:'|plndr/kube-vip:0.1.1'|" \
    | sudo tee /etc/kubernetes/manifests/kube-vip.yaml

以下のような出力がえられると成功です.

apiVersion: v1
kind: Pod
metadata:
  creationTimestamp: null
  name: kube-vip
  namespace: kube-system
spec:
  containers:
  - command:
    - /kube-vip
    - start
    - -c
    - /vip.yaml
    image: 'plndr/kube-vip:0.1.1'
    name: kube-vip
    resources: {}
    securityContext:
      capabilities:
        add:
        - NET_ADMIN
        - SYS_TIME
    volumeMounts:
    - mountPath: /vip.yaml
      name: config
  hostNetwork: true
  volumes:
  - hostPath:
      path: /etc/kube-vip/config.yaml
    name: config
status: {}

kubeadmでのセットアップ

「sudo kubeadm init」コマンドのオプション「----pod-network-cidr」は,クラスタ内ネットワークの構築にFlannelを使用する場合の設定です. 他の方法で構築する場合は以下のKubernetes公式ページを参考に設定してください.

kubernetes.io

また,「--control-plane-endpoint」はkube-vipの${VI_IP_ADDR}を入力する事でkube-apiサーバのエンドポイントを設定し,「--upload-certs」はcontrol plane node間で自動的に証明書の交換を行なってくれるオプション(以前はこのオプションがなく手動で証明書交換を行なっていた.)で,「--apiserver-bind-port」はkube-apiサーバ用ポートです. ここで,kubeadmのドキュメント kubeadm init - Kubernetes にあるように,アップロードされたcontrol plane nodeの証明書は有効期限が2時間しかありませんので注意してください.

$ sudo kubeadm init --control-plane-endpoint ${VI_IP_ADDR}:6443 --pod-network-cidr=10.244.0.0/16 --upload-certs --apiserver-bind-port 6444

W0608 17:35:35.559622   19592 version.go:102] could not fetch a Kubernetes version from the internet: unable to get URL "https://dl.k8s.io/release/stable-1.txt": Get https://storage.googleapis.com/kubernetes-release/release/stable-1.txt: net/http: request canceled while waiting for connection (Client.Timeout exceeded while awaiting headers)
W0608 17:35:35.559725   19592 version.go:103] falling back to the local client version: v1.18.3
W0608 17:35:35.559949   19592 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.3
[preflight] Running pre-flight checks
        [WARNING IsDockerSystemdCheck]: detected "cgroupfs" as the Docker cgroup driver. The recommended driver is "systemd". Please follow the guide at https://kubernetes.io/docs/setup/cri/
[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-m0 kubernetes kubernetes.default kubernetes.default.svc kubernetes.default.svc.cluster.local] and IPs [10.96.0.1 xxx.xxx.xxx.xxx xxx.xxx.xxx.xxx]
[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-m0 localhost] and IPs [xxx.xxx.xxx.xxx 127.0.0.1 ::1]
[certs] Generating "etcd/peer" certificate and key
[certs] etcd/peer serving cert is signed for DNS names [k8s-m0 localhost] and IPs [xxx.xxx.xxx.xxx 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"
[endpoint] WARNING: port specified in controlPlaneEndpoint overrides bindPort in the controlplane address
[kubeconfig] Writing "admin.conf" kubeconfig file
[endpoint] WARNING: port specified in controlPlaneEndpoint overrides bindPort in the controlplane address
[kubeconfig] Writing "kubelet.conf" kubeconfig file
[endpoint] WARNING: port specified in controlPlaneEndpoint overrides bindPort in the controlplane address
[kubeconfig] Writing "controller-manager.conf" kubeconfig file
[endpoint] WARNING: port specified in controlPlaneEndpoint overrides bindPort in the controlplane address
[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"
W0608 17:35:37.730317   19592 manifests.go:225] the default kube-apiserver authorization-mode is "Node,RBAC"; using "Node,RBAC"
[control-plane] Creating static Pod manifest for "kube-scheduler"
W0608 17:35:37.730883   19592 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.578061 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] Storing the certificates in Secret "kubeadm-certs" in the "kube-system" Namespace
[upload-certs] Using certificate key:
ffa4187d479b4543ef2543e458cc5aa791c37b9159defa35e4d463518a871e15
[mark-control-plane] Marking the node k8s-m0 as control-plane by adding the label "node-role.kubernetes.io/master=''"
[mark-control-plane] Marking the node k8s-m0 as control-plane by adding the taints [node-role.kubernetes.io/master:NoSchedule]
[bootstrap-token] Using token: iri98c.vvgys2r619z676d5
[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
[endpoint] WARNING: port specified in controlPlaneEndpoint overrides bindPort in the controlplane address
[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/

You can now join any number of the control-plane node running the following command on each as root:

  kubeadm join xxx.xxx.xxx.xxx:6443 --token hogehoge \
    --discovery-token-ca-cert-hash sha256:hogehoge \
    --control-plane --certificate-key hogehoge

Please note that the certificate-key gives access to cluster sensitive data, keep it secret!
As a safeguard, uploaded-certs will be deleted in two hours; If necessary, you can use
"kubeadm init phase upload-certs --upload-certs" to reload certs afterward.

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

kubeadm join xxx.xxx.xxx.xxx:6443 --token hogehoge \
    --discovery-token-ca-cert-hash sha256:hogehoge

下の方に「Your Kubernetes control-plane has initialized successfully!」と表示されれば成功です. control plane nodeを冗長化する際に「kubeadm join xxx.xxx.xxx.xxx:6443 --token hogehoge \ --discovery-token-ca-cert-hash sha256:hogehoge \ --control-plane --certificate-key hogehoge」を,nodeを追加する際に「kubeadm join xxx.xxx.xxx.xxx:6443 --token hogehoge \ --discovery-token-ca-cert-hash sha256:hogehoge」を使用するのでどこかに保存しておいてください.このtokenの有効期限は24時間のため有効期限が切れた場合は以下のコマンドで生成済みのtokenの有無を確認の上新規発行を行ってください.

$ sudo kubeadm token list
[sudo] michiru のパスワード: 
TOKEN                     TTL         EXPIRES                     USAGES                   DESCRIPTION                                                EXTRA GROUPS
hogehoge   24h         2020-06-09T17:36:00+09:00   authentication,signing   The default bootstrap token generated by 'kubeadm init'.   system:bootstrappers:kubeadm:default-node-token
$ sudo kubeadm token create --print-join-command
kubeadm join xxx.xxx.xxx.xxx:6443 --token hogehogehoge     --discovery-token-ca-cert-hash sha256:hogehogehoge

ここで設定ミスをして,もう一度「sudo kubeadm init」コマンドを実行する場合「sudo kubeadm reset」を実行してkubeadmの設定をリセットしてから再度「sudo kubeadm init」コマンドを実行してください.

次に,kubectlで使用する認証ファイルの準備を行うため、以下のコマンドを実行してください.

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

CNIの設定

以下のコマンドでflannelを用いて構築していきます.

kubectl apply -f https://raw.githubusercontent.com/coreos/flannel/master/Documentation/kube-flannel.yml 

podsecuritypolicy.policy/psp.flannel.unprivileged created
clusterrole.rbac.authorization.k8s.io/flannel created
clusterrolebinding.rbac.authorization.k8s.io/flannel created
serviceaccount/flannel created
configmap/kube-flannel-cfg created
daemonset.apps/kube-flannel-ds-amd64 created
daemonset.apps/kube-flannel-ds-arm64 created
daemonset.apps/kube-flannel-ds-arm created
daemonset.apps/kube-flannel-ds-ppc64le created
daemonset.apps/kube-flannel-ds-s390x created

念の為に正しく設定できているか確認しておきます.

$ kubectl get po -n kube-system
NAME                                   READY   STATUS    RESTARTS   AGE
coredns-66bff467f8-lnp56               1/1     Running   2          9h
etcd-k8s-m0                            1/1     Running   0          9h
kube-apiserver-k8s-m0                  1/1     Running   0          9h
kube-controller-manager-k8s-m0         1/1     Running   1          9h
kube-flannel-ds-amd64-bddvt            1/1     Running   1          9h
kube-proxy-7pqtt                       1/1     Running   0          9h
kube-scheduler-k8s-m0                  1/1     Running   1          9h
kube-vip-k8s-m0                        1/1     Running   0          9h

ここで,すべてのサービスのSTATUSがRunningになっていることを確認してください. 特にcorednsが正しく起動できていない場合,Step01の設定が正しく行えていないことが考えられるためよく確認してください

Step04:2台目以降のcontrol plane nodeのセットアップ

2台目以降control plane nodeの何台追加しても同じ設定になります. kube-vipとkubeadmの実行順序だけ気をつけてください.

kubeadmでのセットアップ

一台目のcontrol plane node構築時に出力されたコマンドを実行するだけです.

$ kubeadm join xxx.xxx.xxx.xxx:6443 --token hogehoge \
    --discovery-token-ca-cert-hash sha256:hogehoge \
    --control-plane --certificate-key hogehoge

[preflight] Running pre-flight checks
[preflight] Reading configuration from the cluster...
[preflight] FYI: You can look at this config file with 'kubectl -n kube-system get cm kubeadm-config -oyaml'
[preflight] Running pre-flight checks before initializing the new control plane instance
[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'
[download-certs] Downloading the certificates in Secret "kubeadm-certs" in the "kube-system" Namespace
[certs] Using certificateDir folder "/etc/kubernetes/pki"
[certs] Generating "etcd/peer" certificate and key
[certs] etcd/peer serving cert is signed for DNS names [k8s-m1 localhost] and IPs [xxx.xxx.xxx.xxx 127.0.0.1 ::1]
[certs] Generating "apiserver-etcd-client" certificate and key
[certs] Generating "etcd/server" certificate and key
[certs] etcd/server serving cert is signed for DNS names [k8s-m1 localhost] and IPs [xxx.xxx.xxx.xxx 127.0.0.1 ::1]
[certs] Generating "etcd/healthcheck-client" certificate and key
[certs] Generating "apiserver" certificate and key
[certs] apiserver serving cert is signed for DNS names [k8s-m1 kubernetes kubernetes.default kubernetes.default.svc kubernetes.default.svc.cluster.local] and IPs [10.96.0.1 xxx.xxx.xxx.xxx xxx.xxx.xxx.xxx]
[certs] Generating "apiserver-kubelet-client" certificate and key
[certs] Generating "front-proxy-client" certificate and key
[certs] Valid certificates and keys now exist in "/etc/kubernetes/pki"
[certs] Using the existing "sa" key
[kubeconfig] Generating kubeconfig files
[kubeconfig] Using kubeconfig folder "/etc/kubernetes"
[kubeconfig] Writing "admin.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"
W0608 17:41:14.871995   10536 manifests.go:225] the default kube-apiserver authorization-mode is "Node,RBAC"; using "Node,RBAC"
[control-plane] Creating static Pod manifest for "kube-controller-manager"
W0608 17:41:14.876431   10536 manifests.go:225] the default kube-apiserver authorization-mode is "Node,RBAC"; using "Node,RBAC"
[control-plane] Creating static Pod manifest for "kube-scheduler"
W0608 17:41:14.877076   10536 manifests.go:225] the default kube-apiserver authorization-mode is "Node,RBAC"; using "Node,RBAC"
[check-etcd] Checking that the etcd cluster is healthy
[kubelet-start] Downloading configuration for the kubelet from the "kubelet-config-1.18" ConfigMap in the kube-system namespace
[kubelet-start] Writing kubelet configuration to file "/var/lib/kubelet/config.yaml"
[kubelet-start] Writing kubelet environment file with flags to file "/var/lib/kubelet/kubeadm-flags.env"
[kubelet-start] Starting the kubelet
[kubelet-start] Waiting for the kubelet to perform the TLS Bootstrap...
[etcd] Announced new etcd member joining to the existing etcd cluster
[etcd] Creating static Pod manifest for "etcd"
[etcd] Waiting for the new etcd member to join the cluster. This can take up to 40s
{"level":"warn","ts":"2020-06-08T17:41:31.138+0900","caller":"clientv3/retry_interceptor.go:61","msg":"retrying of unary invoker failed","target":"passthrough:///https://xxx.xxx.xxx.xxx:2379","attempt":0,"error":"rpc error: code = DeadlineExceeded desc = context deadline exceeded"}
[upload-config] Storing the configuration used in ConfigMap "kubeadm-config" in the "kube-system" Namespace
[mark-control-plane] Marking the node k8s-m1 as control-plane by adding the label "node-role.kubernetes.io/master=''"
[mark-control-plane] Marking the node k8s-m1 as control-plane by adding the taints [node-role.kubernetes.io/master:NoSchedule]

This node has joined the cluster and a new control plane instance was created:

* Certificate signing request was sent to apiserver and approval was received.
* The Kubelet was informed of the new secure connection details.
* Control plane (master) label and taint were applied to the new node.
* The Kubernetes control plane instances scaled up.
* A new etcd member was added to the local/stacked etcd cluster.

To start administering your cluster from this node, 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

Run 'kubectl get nodes' to see this node join the cluster.

続いて以下のコマンドを実行し,kube-configファイルを作成します.

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

kube-vip

ドキュメント Load Balancing a Kubernetes Cluster (Control-Plane) にあるように,2台目以降のcontrol plane nodeをセットアップする際はK8s clusterに参加させるより前にkube-vip静的Pod用マニフェストを準備しては行けません.

2台目以降でも使用するkube-vip静的Pod用yamlファイルは1台目とほとんど変わりませんが,少し変更箇所もあるので以下のように「/etc/kube-vip/config.yaml」を作成してください.

remotePeers:
- id: ${ID_NOT_LOCAL_NODE}
  address: ${IP_ADDR_NOT_LOCAL_NODE}
  port: 10000
- id: ${ID_NOT_LOCAL_NODE}
  address: ${IP_ADDR_NOT_LOCAL_NODE}
  port: 10000
localPeer:
  ${ID_LOCAL_NODE}
  address: ${IP_ADDR_LOCAL_NODE}
  port: 10000
vip: ${VI_IP_ADDR}
gratuitousARP: true
singleNode: false
startAsLeader: false
interface: ${NETWORK_INTERFACE_NAME}
loadBalancers:
- name: Kubernetes Control Plane
  type: tcp
  port: 6443
  bindToVip: true
  backends:
  - port: 6444
    address: ${IP_ADDR_CONTROL_PLANE_NODE}
  - port: 6444
    address: ${IP_ADDR_CONTROL_PLANE_NODE}
  - port: 6444
    address: ${IP_ADDR_CONTROL_PLANE_NODE}

1台目のyamlファイルとの変更点は以下の通りです. また,「LOACL_NODE」がついているものはインストールするホストにしたがって変更してください.

...(省略)
vip: ${VI_IP_ADDR}
gratuitousARP: true
singleNode: false
- startAsLeader: true
+ startAsLeader: false
interface: ${NETWORK_INTERFACE_NAME}
...(省略)

Step05:nodeのセットアップ

nodeの方も,念のためにもう一度スワップ機能をオフにしておきます. その後一台目のcotrol planeで発行されて保存したtokenを入力します. またnodeは何台目であっても設定方法が同じです.

$ kubeadm join xxx.xxx.xxx.xxx:6443 --token hogehoge \
>     --discovery-token-ca-cert-hash sha256:hogehoge
W0608 17:52:22.187551    3076 join.go:346] [preflight] WARNING: JoinControlPane.controlPlane settings will be ignored when control-plane flag is not set.
[preflight] Running pre-flight checks
[preflight] Reading configuration from the cluster...
[preflight] FYI: You can look at this config file with 'kubectl -n kube-system get cm kubeadm-config -oyaml'
[kubelet-start] Downloading configuration for the kubelet from the "kubelet-config-1.18" ConfigMap in the kube-system namespace
[kubelet-start] Writing kubelet configuration to file "/var/lib/kubelet/config.yaml"
[kubelet-start] Writing kubelet environment file with flags to file "/var/lib/kubelet/kubeadm-flags.env"
[kubelet-start] Starting the kubelet
[kubelet-start] Waiting for the kubelet to perform the TLS Bootstrap...

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.

「Run 'kubectl get nodes' on the control-plane to see this node join the cluster.」と最後に表示されればさきほど作成したK8s Clusterへの追加が成功しています.

Step06:最終確認

最後に,追加したnodeがK8s Clusterに追加されたか確認を行うためcontrol plane nodeもしかはkube-configを保持するローカルマシンで以下のコマンドを実行してください.

$ kubectl get nodes
NAME      STATUS   ROLES    AGE   VERSION
eriri     Ready    <none>   8h    v1.18.3
k8s-m0    Ready    master   8h    v1.18.3
k8s-m1    Ready    master   8h    v1.18.3
michiru   Ready    master   8h    v1.18.3
utaha     Ready    <none>   8h    v1.18.3

K8s Cluster上でのDeepLearning環境の構築

この章では,先ほど構築したK8s Cluster上に機械学習環境の構築を行っていきます.

NVIDIA-device-plugin-for-Kubernetesの導入

NVIDIA-device-plugin-for-Kubernetesのdaemonsetをデプロイします. 今回は記事執筆時点で最新のbeta6を使用します.

github.com

$ kubectl apply -f https://raw.githubusercontent.com/NVIDIA/k8s-device-plugin/1.0.0-beta6/nvidia-device-plugin.yml

daemonset.apps/nvidia-device-plugin-daemonset created

次にNVIDIA-device-plugin-for-Kubernetesデーモンセットが正常にデプロイできているかどうか確認します.「nvidia-device-plugin-daemonset-xxxxx」のSTATUSがRUNNINGになっていれば正常にデプロイできています.

セットアップが完了したので,テスト用Podをデプロイして確認します.デプロイするPodの中身は自分でマニフェストファイルを書くか,以下の内容で「sample.yaml」ファイルを作成してください.

piVersion: v1
kind: Pod
metadata:
  name: gpu-pod
spec:
  containers:
    - name: cuda-container
      image: nvidia/cuda:10.0-devel
      tty: true
      resources:
        limits:
          nvidia.com/gpu: 1 # requesting 1 GPUs

マニフェストが完成したら以下のコマンドを実行してgpu-podをデプロイし,デプロイしたPodのStatusがRunnningになっているかどうか確認します.

$ kubectl apply -f sample.yaml
pod/gpu-pod created
$ kubectl get pods --all-namespaces -o wide
NAMESPACE     NAME                                   READY   STATUS    RESTARTS   AGE   IP               NODE    NOMINATED NODE   READINESS GATES
default       gpu-pod                                1/1     Running   0          10s   10.244.1.5       Utaha   <none>           <none>
(省略)

以上で全て完了です.

確認

全ての工程を正しく行えた場合以下のようなPodがkube-system namespaceにデプロイされているはずです.

$ kubectl get po -n kube-system
NAME                                   READY   STATUS    RESTARTS   AGE
coredns-66bff467f8-lnp56               1/1     Running   2          9h
coredns-66bff467f8-tpcdf               1/1     Running   0          9h
etcd-k8s-m0                            1/1     Running   0          9h
etcd-k8s-m1                            1/1     Running   0          9h
etcd-michiru                           1/1     Running   0          9h
kube-apiserver-k8s-m0                  1/1     Running   0          9h
kube-apiserver-k8s-m1                  1/1     Running   0          9h
kube-apiserver-michiru                 1/1     Running   0          9h
kube-controller-manager-k8s-m0         1/1     Running   1          9h
kube-controller-manager-k8s-m1         1/1     Running   0          9h
kube-controller-manager-michiru        1/1     Running   0          9h
kube-flannel-ds-amd64-bddvt            1/1     Running   1          9h
kube-flannel-ds-amd64-cbgx8            1/1     Running   2          9h
kube-flannel-ds-amd64-h6bcv            1/1     Running   2          9h
kube-flannel-ds-amd64-pdjxh            1/1     Running   0          9h
kube-flannel-ds-amd64-xlbjj            1/1     Running   3          9h
kube-proxy-7pqtt                       1/1     Running   0          9h
kube-proxy-b66mj                       1/1     Running   0          9h
kube-proxy-ht7dd                       1/1     Running   0          9h
kube-proxy-klgk4                       1/1     Running   0          9h
kube-proxy-tdzkb                       1/1     Running   0          9h
kube-scheduler-k8s-m0                  1/1     Running   1          9h
kube-scheduler-k8s-m1                  1/1     Running   0          9h
kube-scheduler-michiru                 1/1     Running   0          9h
kube-vip-k8s-m0                        1/1     Running   0          9h
kube-vip-k8s-m1                        1/1     Running   0          9h
kube-vip-michiru                       1/1     Running   0          9h
nvidia-device-plugin-daemonset-245zk   1/1     Running   0          7h16m
nvidia-device-plugin-daemonset-6dcxx   1/1     Running   0          7h16m
nvidia-device-plugin-daemonset-hgkwt   1/1     Running   0          7h16m
nvidia-device-plugin-daemonset-sf7nl   1/1     Running   0          7h16m
nvidia-device-plugin-daemonset-twnsb   1/1     Running   0          7h16m

出力結果を見てみるとetcd,kube-apiserver,kube-controller,kube-schedulerがしっかり冗長構成になっていることがわかります.この記事ではやりませんが,実際にどれか一つのマシンを落としてもきちんとK8s clusterは稼働し続けていることが確認できると思います.

おわりに

今回はkube-vipを用いてcontrol planeの冗長化を行いました. ドキュメント Creating a single control-plane cluster with kubeadm - Kubernetes

By default, your cluster will not schedule Pods on the control-plane node for security reasons.

にあるように プロダクションで使用することを考えるとセキュリティ上の問題からcontrol planeとnodeは分離するべきですが台数を用意することができなかったため共存させる形になりました.誰かの参考になれば幸いです.