まだプログラマーですが何か?

プログラマーネタ中心。たまに作成したウェブサービス関連の話も https://twitter.com/dotnsf

タグ:cluster

以前にラズパイ3(メモリ1GB)を3台使ってコンテナクラスタのオーケストレーション環境を作ったことがありました:
ラズベリーパイと鳩サブレ缶で docker swarm クラスタを構築する
ラズベリーパイと鳩サブレ缶で kubernetes クラスタを構築する

8ea25b3e


上記の上では docker swarm 環境、下では kubernetes 環境を構築しました。手順などの詳細はそれぞれのリンク先を参照していただきたいのですが、この時は kubernetes 1.8 を使ってクラスタを構築しました。kubernetes の環境としては作れたのですが、(案の定というか)メモリが圧倒的に足りなさすぎて実用にはほど遠い kubernetes 環境となってしまいました。マイナーな環境であることは理解しつつ、今でもラズパイ3でのオーケストレーション環境としては docker swarm が現実的なのかなあ、と感じています。


さて時は流れ、ラズパイ4が発売され、日本でも入手できるようになりました。CPU 等の強化もありますが、なんといっても4GBのメモリモデルを選ぶことができるようになったモデルです。上述のメモリ不足が解消される期待ができる上に CPU 等も強化されているわけなので、まともに動く(苦笑) kubernetes 環境構築にも期待できそうです。早速使ってみました。


【環境構築手順】
基本的には特別なことはしていないのですが、一応一通りの説明をしていきます。なお今回のクラスタ環境には計3台のラズパイ4を使って1台のマスターノードと2台のワーカーノードを構築しています。以下ではそれぞれ k8s-master-01, k8s-worker-01, k8s-worker-02 という名称で呼ぶことにします。

【3台共通で行う作業】
全てのノードに共通で行う作業は以下になります:
・Raspbian OS Buster 最新版の導入
・ネットワークなどを設定して再起動
・docker インストール
・kubectl などのコマンドをインストール

まず OS はラズパイ4向けにリリースされた Raspbian OS Buster を使います。こちらのサイトから "Raspbian Buster Lite" と書かれたイメージの最新版をダウンロード&展開して、MicroSD カードに焼き付けてください(私が試した時は "September 2019" バージョンが最新でした):
2019121601


起動後、ネットワークを有効にした上でターミナルから以下のコマンドを実行し、各種モジュールを最新版にしておきます。ここで少し時間がかかります:
$ sudo apt-get update -y

$ sudo apt-get upgrade -y

この後で一度再起動をかけるのですが、その再起動前に変更しておくべき項目がいくつかあります。まずは /boot/cmdline.txt を編集します。これは再起動の後 kubernetes 関連のコマンドを導入して動かす際に必要な cgroups の設定です:
$ sudo vi /boot/cmdline.txt

(cgroup_enable=cpuset cgroup_enable=memory cgroup_memory=1 を追加)

同様にして再起動前に以下のコマンドを実行してスワップメモリを無効&再起動後も無効に設定します。最近の kubernetes はスワップメモリがあると起動に失敗するので、予め無効にしておくための設定です:
$ sudo swapoff --all

$ sudo apt-get purge -y --auto-remove dphys-swapfile

$ sudo rm -fr /var/swap

また IP アドレスやホスト名の設定も行っておきましょう。以下の説明では3台のラズパイがそれぞれ以下のような固定 IP アドレス及びホスト名で動かす想定とします(IP アドレスなど設定が異なる場合は適宜読み替えてください):
ホスト名IPアドレスノードの用途
k8s-master-01192.168.1.200マスター
k8s-worker-01192.168.1.201ワーカー
k8s-worker-02192.168.1.202ワーカー


まず /etc/dhcpcd.conf を編集して以下の部分を追加し、固定 IP アドレスを取得するように変更します:
$ sudo vi /etc/dhcpcd.conf

(以下は無線 LAN(wlan0) で 192.168.1.200 に設定する場合)
interface wlan0
static ip_address=192.168.1.200/24
static routers=192.168.1.1
static domain_name_servers=192.168.1.1 8.8.8.8

次に /etc/hostname を編集して、raspberrypi と書かれた内容をそれぞれのホスト名で書き換えます:
$ sudo vi /etc/hostname

(k8s-master-01 の場合)
raspberrypi  k8s-master-01

また /etc/hosts を編集して、他の2ノードにも名前でアクセスできるように名前解決ルールを記載しておきます:
$ sudo vi /etc/hostname

(k8s-master-01 の場合)
127.0.1.1   raspberrypi  k8s-master-01

(以下は3台全てに追加する3行)
192.168.1.200  k8s-master-01
192.168.1.201  k8s-worker-01
192.168.1.202  k8s-worker-02

また以下は必須ではないのですが、固定の IP アドレスを設定する場合は SSH でアクセスできるようになるとこの後の作業が(直接ログインの必要がなくなり、リモートから SSH 接続後に作業できるようになって)楽なので、SSH 接続を有効にしておくと便利です。sudo raspi-config を実行してから、"Interfacing options" - "SSH" を選択して、SSH を有効にしておきます:
2019121602


ここまでの作業が済んだら一度再起動をかけます:
$ sudo shutdown -r now

再起動後、再度(SSH などで)ログインして、SSH 鍵を3台のラズパイ間で共有します。この作業は3台のラズパイが全て(上述の IP アドレスの再設定などを行った上での)再起動をして、全てネットワークに接続して稼働している状態で3台全てで行う必要があります:
$ ssh-keygen -t rsa

(いろいろ聞かれるけど、全て無指定のまま Enter でもOK)
$ ssh-copy-id k8s-master-01 $ ssh-copy-id k8s-worker-01 $ ssh-copy-id k8s-worker-02

次に docker を導入します:
$ curl -sSL https://get.docker.com/ | sh

$ sudo usermod -aG docker pi

(この後、一度ターミナルをログアウトして抜けて、再度ターミナルを開く)

次に kubelet, kubeadm, kubectl, kubenetes-cni などをインストールしますが、その準備として新しいリポジトリを登録する必要があります。まずはその準備として apt コマンドを https のリポジトリでも実行できるよう環境を用意しておきます:
$ sudo apt-get install -y apt-transport-https

そしてリポジトリのソースリストに kubernetes を追加します:
$ curl -fsSL 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 /etc/apt/sources.list.d/kubernetes.list

$ sudo apt-get update

ここまでの準備で kubectl などのコマンドを導入することができるようになったので、まとめて導入し、更にバージョンが変更されることがないよう固定します:
$ sudo apt-get update

$ sudo apt-get install kubelet kubeadm kubectl kubernetes-cni

$ sudo apt-mark hold kubelet kubeadm kubectl kubernetes-cni

3台共通で行う導入作業は以上です。


【マスターノード上で行う作業】
マスターノード上では flannel を使って kubernetes クラスタのコントロールプレーンを初期化します(このコマンド終了後に(kubeadm join)で始まる行が表示されるので、その内容を保存しておきます):
$ sudo kubeadm init --pod-network-cidr=10.244.0.0/16

上述コマンドの実行結果に含まれるコマンドを実行します:
$ mkdir -p $HOME/.kube

$ sudo cp -i /etc/kubernetes/admin.conf $HOME/.kube/config

$ sudo chown $(id -u):$(id -g) $HOME/.kube/config

最後にコンテナ間通信のためのモジュール flannel を導入します:
$ kubectl apply -f https://raw.githubusercontent.com/coreos/flannel/62e44c867a2846fefb68bd5f178daf4da3095ccb/Documentation/kube-flannel.yml

これでマスターノード上の作業は完了です。以下の kubectl version コマンドを実行して正しく結果が返ってくることを確認しておきます:
$ kubectl version

Client Version: version.Info{Major:"1", Minor:"17", GitVersion:"v1.17.0", GitCommit:"70132b0f130acc0bed193d9ba59dd186f0e634cf", GitTreeState:"clean", BuildDate:"2019-12-07T21:20:10Z", GoVersion:"go1.13.4", Compiler:"gc", Platform:"linux/arm"}
Server Version: version.Info{Major:"1", Minor:"17", GitVersion:"v1.17.0", GitCommit:"70132b0f130acc0bed193d9ba59dd186f0e634cf", GitTreeState:"clean", BuildDate:"2019-12-07T21:12:17Z", GoVersion:"go1.13.4", Compiler:"gc", Platform:"linux/arm"}


【ワーカーノード上で行う作業】
続いて上述の kubeadm init コマンド正常終了時に出ていた、kubeadm join で始まるコマンドをワーカーノード上で実行すると、用意したマスターノードにワーカーノードが追加される形になります:
$ sudo kubeadm join 192.168.1.200:6443 --token XXXXXXXXXXXXXXXXX --discovery-token-ca-cert-hash sha256:XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX

全てのワーカーノードが Ready ステータスになるまで少し時間がかかりますが、これでワーカーノードがマスターノードに紐付けられる形で追加されます。この状態になっていることを確認するためにマスターノード上で kubectl get nodes コマンドを実行して、2つのワーカーノードが追加されていることを確認します:
$ kubectl get nodes

NAME            STATUS   ROLES    AGE     VERSION
k8s-master-01   Ready    master   4d7h    v1.17.0
k8s-worker-01   Ready             2d23h   v1.17.0
k8s-worker-02   Ready             2d23h   v1.17.0

これで3台のラズパイ(4)を使った kubernetes クラスタ環境が構築できました。


【動作確認手順】
ではこの環境を使って実際にアプリケーションを動かしてみます。動作確認用にラズパイ(arm32v7)アーキテクチャ用のウェブアプリケーション docker イメージを以下に用意しました。よかったら使ってください:
https://hub.docker.com/repository/docker/dotnsf/hostname

↑このアプリケーションは実行している環境内のファイルシステムから /etc/hostname を読み込んで、そのまま text/plain で表示する(つまり VM やコンテナの内部的なホスト名を返す)だけのシンプルなアプリケーションです。なお特に指定しない場合は 3000 番ポートで待ち受けます。ソースコードに興味がある人は以下を参照ください:
https://github.com/dotnsf/hostname


ではこのイメージを構築した kubernetes 環境内で動かしてみます。以下は全てマスターノード内のターミナルで行う作業です。最初にアプリケーションを hostname という名前で deploy して、3000 番ポートを expose します。続けて pod の状態を確認して(下の例では hostname-86cfdc6cbf-psgdp という名前で動いています)、外部アクセス用のポート番号を確認します(下の例では 30652 番ポートで公開されています):
$ kubectl run hostname --image=dotnsf/hostname

$ kubectl expose deployment hostname --type="NodePort" --port=3000

$ kubectl get pods

NAME                        READY   STATUS    RESTARTS   AGE
hostname-86cfdc6cbf-psgdp   1/1     Running   0          2m12s

$ kubectl get services

NAME         TYPE        CLUSTER-IP      EXTERNAL-IP   PORT(S)          AGE
hostname     NodePort    10.96.124.229                 3000:30652/TCP   2m22s
kubernetes   ClusterIP   10.96.0.1                     443/TCP          4d7h

deploy された hostname-86cfdc6cbf の各 pod がどのワーカーノードで動いているかを確認します(下の例では k8s-worker-02 で動いていることが確認できます):
$ kubectl describe pod hostname-86cfdc6cbf

Name:         hostname-86cfdc6cbf-psgdp
Namespace:    default
Priority:     0
Node:         k8s-worker-02/192.168.1.202
Start Time:   Tue, 17 Dec 2019 07:55:38 +0900
Labels:       pod-template-hash=86cfdc6cbf
              run=hostname
Annotations:  <none>
Status:       Running
IP:           10.244.2.7
IPs:
  IP:           10.244.2.7
Controlled By:  ReplicaSet/hostname-86cfdc6cbf
Containers:
  hostname:
    Container ID:   docker://40fd3c5402c7617aa390ef73acdc6d29502788fd828d8307d1d06f9a00c3081c
    Image:          dotnsf/hostname
    Image ID:       docker-pullable://dotnsf/hostname@sha256:5052df05816b24f6b27da8e3ef75a912944747234118fe25d7bd054f841ee6f0
    Port:           <none>
    Host Port:      <none>
    State:          Running
      Started:      Tue, 17 Dec 2019 07:56:23 +0900
    Ready:          True
    Restart Count:  0
    Environment:    <none>
    Mounts:
      /var/run/secrets/kubernetes.io/serviceaccount from default-token-6qsnh (ro)
Conditions:
  Type              Status
  Initialized       True
  Ready             True
  ContainersReady   True
  PodScheduled      True
Volumes:
  default-token-6qsnh:
    Type:        Secret (a volume populated by a Secret)
    SecretName:  default-token-6qsnh
    Optional:    false
QoS Class:       BestEffort
Node-Selectors:  <none>
Tolerations:     node.kubernetes.io/not-ready:NoExecute for 300s
                 node.kubernetes.io/unreachable:NoExecute for 300s
Events:          <none>

アプリケーションにアクセスして、アプリケーションの動作を確認してみます。上述の手順で確認した IP アドレス(ホスト名)とポート番号を使って curl コマンドでアクセスしてみます:
$ curl http://k8s-worker-02:30652/

hostname-86cfdc6cbf-psgdp


期待通りの結果(/etc/hostname の中身)が返ってきました。どうやら構築したラズパイ4の kubernetes クラスタ環境が正しく動作していることを確認できました!


最後に、 deploy したアプリケーションがこの状態では1インスタンスで動作していますが、これを3インスタンスで動作するようにスケールさせてみます:
$ kubectl scale --replicas=3 deployment hostname

各 Pods の状態を確認して、3インスタンスでの動作に切り替わったことを確認します:
$ kubectl get pods

NAME                        READY   STATUS    RESTARTS   AGE
hostname-86cfdc6cbf-gh88s   1/1     Running   0          7m3s
hostname-86cfdc6cbf-h4x4z   1/1     Running   0          7m3s
hostname-86cfdc6cbf-psgdp   1/1     Running   0          16h

もともと hostname-86cfdc6cbf-psgdp 上だけで動いていたのですが、この Pod に加えて hostname-86cfdc6cbf-gh88s と hostname-86cfdc6cbf-h4x4z の2つの pods が追加されたことが確認できました。


改めて各インスタンスがどのワーカーノードで動いているのかを確認します:
$ kubectl describe pod hostname-86cfdc6cbf

Name:         hostname-86cfdc6cbf-gh88s
Namespace:    default
Priority:     0
Node:         k8s-worker-01/192.168.1.201
Start Time:   Wed, 18 Dec 2019 00:26:36 +0900
Labels:       pod-template-hash=86cfdc6cbf
              run=hostname
Annotations:  <none>
Status:       Running
IP:           10.244.1.9
IPs:
  IP:           10.244.1.9
Controlled By:  ReplicaSet/hostname-86cfdc6cbf
Containers:
  hostname:
    Container ID:   docker://923d7727f8c0fbcc7af3ee5119e60cc22bd2a0817e56e5230879df650edbdc0f
    Image:          dotnsf/hostname
    Image ID:       docker-pullable://dotnsf/hostname@sha256:5052df05816b24f6b27da8e3ef75a912944747234118fe25d7bd054f841ee6f0
    Port:           <none>
    Host Port:      <none>
    State:          Running
      Started:      Wed, 18 Dec 2019 00:27:33 +0900
    Ready:          True
    Restart Count:  0
    Environment:    <none>
    Mounts:
      /var/run/secrets/kubernetes.io/serviceaccount from default-token-6qsnh (ro)
Conditions:
  Type              Status
  Initialized       True
  Ready             True
  ContainersReady   True
  PodScheduled      True
Volumes:
  default-token-6qsnh:
    Type:        Secret (a volume populated by a Secret)
    SecretName:  default-token-6qsnh
    Optional:    false
QoS Class:       BestEffort
Node-Selectors:  <none>
Tolerations:     node.kubernetes.io/not-ready:NoExecute for 300s
                 node.kubernetes.io/unreachable:NoExecute for 300s
Events:
  Type    Reason     Age        From                    Message
  ----    ------     ----       ----                    -------
  Normal  Scheduled  <unknown>  default-scheduler       Successfully assigned default/hostname-86cfdc6cbf-gh88s to k8s-worker-01
  Normal  Pulling    2m29s      kubelet, k8s-worker-01  Pulling image "dotnsf/hostname"
  Normal  Pulled     2m27s      kubelet, k8s-worker-01  Successfully pulled image "dotnsf/hostname"
  Normal  Created    2m3s       kubelet, k8s-worker-01  Created container hostname
  Normal  Started    117s       kubelet, k8s-worker-01  Started container hostname

Name:         hostname-86cfdc6cbf-h4x4z
Namespace:    default
Priority:     0
Node:         k8s-worker-02/192.168.1.202
Start Time:   Wed, 18 Dec 2019 00:26:36 +0900
Labels:       pod-template-hash=86cfdc6cbf
              run=hostname
Annotations:  <none>
Status:       Running
IP:           10.244.2.8
IPs:
  IP:           10.244.2.8
Controlled By:  ReplicaSet/hostname-86cfdc6cbf
Containers:
  hostname:
    Container ID:   docker://f2af91199fa35f6e64d717c3f101ca2e559d3136196519e73cad948a2708527a
    Image:          dotnsf/hostname
    Image ID:       docker-pullable://dotnsf/hostname@sha256:5052df05816b24f6b27da8e3ef75a912944747234118fe25d7bd054f841ee6f0
    Port:           <none>
    Host Port:      <none>
    State:          Running
      Started:      Wed, 18 Dec 2019 00:27:28 +0900
    Ready:          True
    Restart Count:  0
    Environment:    <none>
    Mounts:
      /var/run/secrets/kubernetes.io/serviceaccount from default-token-6qsnh (ro)
Conditions:
  Type              Status
  Initialized       True
  Ready             True
  ContainersReady   True
  PodScheduled      True
Volumes:
  default-token-6qsnh:
    Type:        Secret (a volume populated by a Secret)
    SecretName:  default-token-6qsnh
    Optional:    false
QoS Class:       BestEffort
Node-Selectors:  <none>
Tolerations:     node.kubernetes.io/not-ready:NoExecute for 300s
                 node.kubernetes.io/unreachable:NoExecute for 300s
Events:
  Type    Reason     Age        From                    Message
  ----    ------     ----       ----                    -------
  Normal  Scheduled  <unknown>  default-scheduler       Successfully assigned default/hostname-86cfdc6cbf-h4x4z to k8s-worker-02
  Normal  Pulling    2m27s      kubelet, k8s-worker-02  Pulling image "dotnsf/hostname"
  Normal  Pulled     2m24s      kubelet, k8s-worker-02  Successfully pulled image "dotnsf/hostname"
  Normal  Created    2m2s       kubelet, k8s-worker-02  Created container hostname
  Normal  Started    119s       kubelet, k8s-worker-02  Started container hostname

Name:         hostname-86cfdc6cbf-psgdp
Namespace:    default
Priority:     0
Node:         k8s-worker-02/192.168.1.202
Start Time:   Tue, 17 Dec 2019 07:55:38 +0900
Labels:       pod-template-hash=86cfdc6cbf
              run=hostname
Annotations:  <none>
Status:       Running
IP:           10.244.2.7
IPs:
  IP:           10.244.2.7
Controlled By:  ReplicaSet/hostname-86cfdc6cbf
Containers:
  hostname:
    Container ID:   docker://40fd3c5402c7617aa390ef73acdc6d29502788fd828d8307d1d06f9a00c3081c
    Image:          dotnsf/hostname
    Image ID:       docker-pullable://dotnsf/hostname@sha256:5052df05816b24f6b27da8e3ef75a912944747234118fe25d7bd054f841ee6f0
    Port:           <none>
    Host Port:      <none>
    State:          Running
      Started:      Tue, 17 Dec 2019 07:56:23 +0900
    Ready:          True
    Restart Count:  0
    Environment:    <none>
    Mounts:
      /var/run/secrets/kubernetes.io/serviceaccount from default-token-6qsnh (ro)
Conditions:
  Type              Status
  Initialized       True
  Ready             True
  ContainersReady   True
  PodScheduled      True
Volumes:
  default-token-6qsnh:
    Type:        Secret (a volume populated by a Secret)
    SecretName:  default-token-6qsnh
    Optional:    false
QoS Class:       BestEffort
Node-Selectors:  <none>
Tolerations:     node.kubernetes.io/not-ready:NoExecute for 300s
                 node.kubernetes.io/unreachable:NoExecute for 300s
Events:          <none>


この例では hostname-86cfdc6cbf-gh88s が k8s-worker-01 上で、それ以外の hostname-86cfdc6cbf-h4x4z 、 hostname-86cfdc6cbf-gh88s は k8s-worker-02 上でそれぞれ動作することがわかりました。

では今度は k8s-worker-01 に対してアクセスしてみます:
$ curl http://k8s-worker-01:30652/
hostname-86cfdc6cbf-gh88s

k8s-worker-01 ノードへも正しくアクセスすることができるようになりました。ここまで、無線 LAN を使ったことが原因(と思われる)パフォーマンスの遅さを感じることはありましたが、ラズパイ3の頃よりはかなり速くなっています! どうやらラズパイ・クラスタ環境の構築および動作確認ができました! あとはこれをクラスタ構築用の鳩サブレ缶にセットしてあげれば完成です:
2019121800


なお、作成した deployment や service を削除するには以下のコマンドを実行します:
$ kubectl delete deployment hostname

$ kubectl delete service hostname



先日紹介したこの記事の続き、というか、本命版です:
ラズベリーパイと鳩サブレ缶で docker swarm クラスタを構築する

豊島屋の鳩サブレ缶がちょうどラズベリーパイ(以下「ラズパイ」)を4つ格納できるような仕切りになっていることに気付いて、この鳩サブレ缶とラズパイでクラスタ環境を作れないか、と思い立ったことがきっかけでした。先日は docker swarm のクラスタを構築しましたが、今回は kubernetes のクラスタを構築します:
20190117


なお、構築手順の途中までは先日と全く同じです。具体的にはこのページでも紹介している3台のラズパイそれぞれに docker を導入する所までは同じ手続きを行います。なお、ここまでの作業が完了している前提で以下を紹介します。


【スワップメモリの無効化】
ここからが Kubernetes 環境のための手順となります。まず Kubernetes 1.8 以降ではスワップメモリが有効な環境下では kubelet が起動しないため、3台のラズパイそれぞれでスワップメモリを無効にします:
$ sudo dphys-swapfile swapoff

$ sudo dphys-swapfile uninstall

$ sudo update-rc.d dphys-swapfile remove


【kubeadm, kubectl, kubelet のインストール】
いよいよ Kubernetes をインストールします。具体的には kubeadm, kubectl, kubelet を導入するのですが、まずはリポジトリを登録&更新します:
$ curl -fsSL 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 /etc/apt/sources.list.d/kube.list

$ sudo apt-get update

そして kubeadm, kubectl, kubelet をインストールします。ただこちらで検証した限りでは kubernetes のバージョン 1.8 以下しか動作しませんでした。というわけでバージョン 1.8 を指定して kubernetes コマンド群をインストールします:
$ sudo apt-get install kubelet=1.8.14-00 kubeadm=1.8.14-00 kubectl=1.8.14-00 kubernetes-cni=0.5.1-00

これでラズパイノードに Kubernetes のコマンドが導入できました。ここまでの作業は3台それぞれで実施する必要があります。


【マスターノードの準備】
Kubernetes のマスターノードを作成します。今回は3台のラズパイの中の raspi001 をマスターノードとするので、以下のコマンドを raspi001 でのみ実施します。またその結果、この前に導入した docker のバージョンとの不整合が起きてしまうので、インストールコマンド時に「バージョンチェックをしない」ための --skip-preflight-checks オプションを指定します:
$ sudo kubeadm init --pod-network-cidr=10.244.0.0/16 --skip-preflight-checks

成功したら、成功時の画面に表示される以下のコマンドを順次実行して pod network をデプロイします:
$ mkdir -p $HOME/.kube

$ sudo cp -i /etc/kubernetes/admin.conf $HOME/.kube/config

$ sudo chown $(id -u):$(id -g) $HOME/.kube/config

後は flannnel をデプロイします:
$ kubectl apply -f https://raw.githubusercontent.com/coreos/flannel/bc79dd1505b0c8681ece4de4c0d86c5cd2643275/Documentation/kube-flannel.yml

これでマスターノードの準備は完了です。


【ワーカーノードの準備】

2台のワーカーノードをマスターノードに接続します。マスターノードの sudo kubeadm init コマンドを実行して成功した時に表示される kubeadm join コマンドを各ワーカーノードで実行します:
$ kubeadm join --token XXXXXX....XXXXXX 192.168.10.101:6443 --discovery-token-ca-cert-hash sha256:ZZZZZZZZZZZZ....ZZZZZZZZZZZZZZZ

【接続状態を確認】

マスターノードで "kubectl version" コマンドを実行してサーバーとクライアントのバージョンを、"kubectl get nodes" コマンドを実行して各ノードの状態が表示されることを確認します:
20190128


なんとかラズパイで kubernetes クラスタ環境が作れました。


 
(参考)
https://qiita.com/hatotaka/items/48a88ecb190e1f5e03c3

https://qiita.com/MahoTakara/items/2b39e06f077927bafa2c



きっかけはこのツイートでも紹介した、豊島屋の鳩サブレ缶がちょうどラズベリーパイを4つ格納できるような仕切りになっていることに気付いたことでした:
20190117


面白そうなので、本当にラズパイのケースにしてみよう、それもせっかくなのでこの鳩サブレ缶の中だけでクラスタリング環境を作ることに挑戦してみよう、と思いたちました。ちなみに私自身はクラスタ化とか専門ではなく、あまり得意な方ではないです。思いついた時点では構築方法として Docker Swarm か、Kubernetes か、まあその辺りが動けばいいな、くらいに考えていました。。

当初は「さてとりあえずラズパイを4台買って・・」と思っていましたが、冷静に考えるとスイッチングハブとか、USBハブとか、電源周りとか、ラズパイ本体以外でもそこそこのサイズの機器を収納する必要がありそうだと気づきました。というわけで、それらをどこか1つにまとめるとして、ラズパイ本体は3台体制で行けるかな・・・ という想定でお買い物しました:
IMG_3782


購入したモノリスト:
買ったモノ個数目的
Raspberry Pi 3 Model B3クラスタノードになるホスト
マイクロ USB ケーブル上記ラズパイの電源ケーブル
マイクロSDカード(64GB)上記ラズパイのストレージ
ラズパイケースプラスチックの仕切りの上にラズパイ本体を直接載せるのは熱的にまずいかと思ったので・・・
LANケーブル3台のラズパイを同一ネットワークに接続(結局、今回は使わず※)
スイッチングハブ1LANケーブルのハブ(結局、今回は使わず※)
USB ハブ電源ケーブルのハブ(結局、今回は使わず※)


※今回は結局無線 LAN で接続することにしたので有線の LAN ケーブルとハブを使わずに構築しました。また USB ハブは購入したのですが、この USB ハブ1つに3台のラズパイを接続すると電流量が足りませんでした。というわけで別途 USB からの電源変換プラグ3つと、テーブルタップ1つを買い足しています。

上記に含まれていないものとして、マイクロ SD カードにイメージを書き込むための Windows PC と、マイクロ SD カードを読み書きするための USB アダプタ、USB -> 電源プラグの変換アダプタ、各ホストでネットワークが有効になるまでの操作用に LCD モニタ、HDMI ケーブル、USB キーボードと USB マウスを使います(これらは所有していたものをそのまま使います)。加えてケースとなる 18 枚入りの鳩サブレー缶を用意しました。

また構築する環境は最近流行り(遅い?)の Kubernetes 、にしたかったのですが、訳あって Docker Swarm にしました。コンテナ・オーケストレーションとしてはほぼデファクトスタンダートになった Kubernetes を選びたかったのですが、とりあえず 2019/01/21 時点ではマスターノードを初期化する kubeadm init コマンドを実行した際に再現率 100% で Timeout エラーになってしまうようでした。


どうもこの Issue に近い現象だと思っています:
2019012101


ただ Issue そのものは Closed になっていて、でもスレッドを見ていると本当に治っているのかなんとも微妙・・ 一方で成功例も報告されているのでラズパイで発生する環境依存問題なのか、タイミングが悪かったのか・・・ なんとも言えないのですが、仮に Kubernetes の障害であったとしてもその修正を待つのも変なので、Docker Swarm によるクラスタリング環境構築を優先することに決めました。

というわけで、改めて3台のラズパイを使った Docker Swarm 環境の構築を目標に作業を進めてみます:
2019012200



【構成図】
このような最終型を目指すことにします:
2018012201

raspi001, raspi002, raspi003 の3台のラズベリーパイを Docker Swarm でクラスタリングします。raspi001 を管理ノード、raspi002 と raspi003 をワーカーノードとします。



【ラズパイノードの準備】
Docker Swarm 環境の核となる3台のノードをラズパイで作ります。前提として最新版の Raspbian OS をマイクロ SD カードに書き込んで起動し、初回起動時のセットアップは3台ぶん済んでいるものとします。また SSH も有効にしておいてください。そして以下の手順を3台それぞれに対して行います。

【ホスト名の変更】
今回は3台のラズパイに raspi001, raspi002, raspi003 という3つの名前をそれぞれ付与して使いますこのため、まずは各ラズパイのホスト名をそれぞれ変更します。
$ sudo vi /etc/hostname

raspberrypiraspi001  raspberrypi をホスト名に変更して保存


また raspi001, raspi002, raspi003 の名前でアクセスできるよう、/etc/hosts も変更しておきます(DNS などが有効であればそちらで対応いただくのがいいと思います):
$ sudo vi /etc/hosts

127.0.1.1       raspberrypiraspi001  raspberrypi をホスト名に変更

192.168.10.101  raspi001
192.168.10.102  raspi002
192.168.10.103  raspi003   raspi00X の名前でアクセスできるよう IP アドレスを指定して追加し保存

【SSH 鍵の共有】
この3台のラズパイ間ではパスワードなしで SSH 接続ができるように SSH 鍵を共有しておきます:
$ ssh-keygen -t rsa
$ ssh-copy-id raspi001
$ ssh-copy-id raspi002
$ ssh-copy-id raspi003

【Docker のインストール】
こちらで紹介した手順でラズパイに Docker をインストールします:
$ curl -sSL https://get.docker.com | sh

$ sudo usermod -aG docker pi

(このコマンドの後、一度ログアウトして再ログインする)

以上、ここまでの手順は3台のラズパイ全てで共通に行っておく必要があります。ちと面倒ですが、全てのラズパイに Docker をインストールするところまで実行しておきます。


【管理ノードの作成】
ここからは3台のラズパイの役割ごとに作業が変わる部分になります。まずは管理ノードを作成します。raspi001 において、自分のアドレスを指定して以下のコマンドを実行します:
$ docker swarm init --advertise-addr 192.168.10.101

Swarm initialized: current node (xenxp2w34x5dhvtdmq0ndwolk) is now a manager.

To add a worker to this swarm, run the following command:

    docker swarm join --token SWMTKN-1-0dg4im1kvb41odjj9zmhwug0bcvg6n8tt0d8548jxho5cp8cvo-df89pmegbdf4a3vy37apftv8r 192.168.10.101:2377

To add a manager to this swarm, run 'docker swarm join-token manager' and follow the instructions.


↑コマンドが成功すると青字のような出力になります。この中の太字部分(docker swarm join で始まる行)は後でワーカーノードで実行することになるコマンドなのでコピペできるようにしておきましょう。


【ワーカーノードの接続】
次にワーカーノード側(raspi002, raspi003)から管理ノードに接続します。上記の管理ノード作成時に実行したコマンドの実行結果をそのまま入力して実行します:
$ docker swarm join --token SWMTKN-1-0dg4im1kvb41odjj9zmhwug0bcvg6n8tt0d8548jxho5cp8cvo-df89pmegbdf4a3vy37apftv8r 192.168.1.153:2377

コマンドの実行が成功すると "This node joined a swarm as a worker." といったメッセージが表示されます。これを raspi002 と raspi003 の両方で(4台以上で構成する場合は全てのワーカーノードで)実行します。


【ノードの状態を確認】
ここまでの作業でこの環境が作れています:

2018012201


ノードの状態を確認してみます。raspi001 から以下のコマンドを実行します:
2019012202

$ docker node ls
ID                            HOSTNAME            STATUS              AVAILABILITY        MANAGER STATUS      ENGINE VERSION
xenxp2w34x5dhvtdmq0ndwolk *   raspi001            Ready               Active              Leader              18.09.0
hztpov7qya7y99m4oifx73ugt     raspi002            Ready               Active                                  18.09.0
wksx0mlyu9wsilk9c4jigvcib     raspi003            Ready               Active                                  18.09.0

実行結果↑の青字部分をみると raspi001, raspi002, raspi003 がリストされ、全て Active になっています。また raspi001 の MANAGER STATUS が Leader とマークされており、ここが管理ノードになっていることが確認できました。


あとはこの3つのラズパイと電源ケーブル等をつなげて鳩サブレー缶に押し込みます:
IMG_3802


鳩サブレ版の(?) docker swarm 環境が完成しました!
IMG_3800



(参考)
https://blog.ruanbekker.com/blog/2018/10/23/setting-up-a-docker-swarm-cluster-on-3-raspberrypi-nodes/


2台の CentOS サーバーで MySQL クラスタ環境を構築する機会があったので、その作業メモです。


前提条件として、2台の CentOS 6.5(x64_86) をそれぞれ A, B とします。それぞれの IP アドレスを AAA.AAA.AAA.AAA / BBB.BBB.BBB.BBB とします。これらにそれぞれ MySQL のデータノードと SQL ノードを構築します。A, B には MySQL 関連モジュールの導入は一切行っていないものとします。
なおサーバー A は管理ノードを兼ねることにします。以下の手順は2台構成ですが、3台目以降を追加する場合は B と同じ手順を行うことになります。

【手順1 MySQL Cluster をインストール(A, B)】
A/B 両方のサーバーに MySQL Cluster をインストールします。CentOS 6.x では MySQL Cluster は標準の yum リポジトリには含まれていないようなので、公式サイトからソースをダウンロードしてインストールします:
Download MySQL Cluster

上記サイトから最新版のインストールセットをダウンロードします。今回の前提環境(CentOS 6.5 64bit)であれば Platform に "Linux Generic" を選択して 64bit 版のモジュールファイルをダウンロードします(ダウンロード時に Oracle のアカウントが必要になります):
2014052801

ちなみに 2014/05/28 現在、この方法で取得すると MySQL Cluster 7.3.5(ファイル名 mysql-cluster-gpl-7.3.5-linux-glibc2.5-x86_64.tar.gz)がダウンロードできます。以下、このモジュールを使っている前提で記載を続けます。バージョン差異などでファイル名が異なる場合は適宜読み替えてください。

ダウンロードしたモジュールファイルを A, B 両方にコピーします(/tmp にコピーしたと仮定します)。そして以下の導入作業を A, B 双方で行います。

サーバーに root でログインし、まず mysql 用のユーザーとグループを作成します:
# groupadd mysql
# useradd -g mysql mysql

コピーした MySQL Cluster インストールモジュールを /usr/local/ 以下に展開し、mysql という名称でアクセスできるようにシンボリックリンクを張ります:
# cd /usr/local
# tar xzvf /tmp/mysql-cluster-gpl-7.3.5-linux-glibc2.5-x86_64.tar.gz
# ln -s mysql-cluster-gpl-7.3.5-linux-glibc2.5-x86_64 mysql

ファイルオーナーとファイルグループを変更します:
# chown -R root.mysql mysql-cluster-gpl-7.3.5-linux-glibc2.5-x86_64
# chown -R mysql.mysql mysql/data/

/usr/local/mysql/bin に PATH を通します:
(例)こういう結果になるよう ~/.bash_profile を編集する
# cat ~/.bash_profile
  :
  :
PATH=$PATH:$HOME/bin:/usr/local/mysql/bin
  :
  :

MySQL の管理用データベースを初期化して、デーモン起動用スクリプトをコピーして用意します:
# cd /usr/local/mysql
# ./scripts/mysql_install_db --user=mysql
# cp /usr/local/mysql/support-files/mysql.server /etc/init.d/mysql
# chown root.root /etc/init.d/mysql
# chmod u+x /etc/init.d/mysql

これでモジュールの導入は完了です、意外と簡単。この処理は全てのサーバー(今回の例であれば A と B)に対して行います。


管理ノードではないサーバー(B) のみ、/var/lib/mysql-cluster/ というフォルダを作成しておきます。中身は不要ですが MySQL が利用するのでファイルオーナーとグループオーナーを変更しておきます:
# mkdir /var/lib/mysql-cluster
# chown -R mysql.mysql /var/lib/mysql-cluster

管理ノードサーバー(A) に関しては、この作業は次の手順の中に含まれているので不要です。


【手順2 MySQL Cluster 管理ノードの設定(Aのみ)】
次に管理ノードの設定を行います。この作業はサーバー A に対してのみ行います。

今回は設定ファイルを /var/lib/mysql-cluster/ 以下に作成します。場所は任意で構いませんが、変更する場合は適宜読み替えてください。
# mkdir /var/lib/mysql-cluster
# chown -R mysql.mysql /var/lib/mysql-cluster
# vi /var/lib/mysql-cluster/config.ini
  :
  :
[NDB_MGMD]
hostname=AAA.AAA.AAA.AAA  # 管理サーバー(サーバーA)のIPアドレス

[NDBD default]
NoOfReplicas=2  # クラスタ環境の冗長化数(台数)
datadir=/var/lib/mysql-cluster

[NDBD]
hostname=AAA.AAA.AAA.AAA  # 1台目のデータノードの IP アドレス
datadir=/var/lib/mysql-cluster

[NDBD]
hostname=BBB.BBB.BBB.BBB  # 2台目のデータノードの IP アドレス
datadir=/var/lib/mysql-cluster

[MYSQLD]
hostname=AAA.AAA.AAA.AAA  # 1台目のSQLノードの IP アドレス

[MYSQLD]
hostname=BBB.BBB.BBB.BBB  # 2台目のSQLノードの IP アドレス


【手順3 MySQL Cluster SQL ノードの設定(A, B)】
次にSQLノードの設定を行います。この作業はサーバー A, B 両方に対して行います。

具体的には /etc/my.cnf をクラスタ環境向けに編集します:
# vi /etc/my.cnf
  :
  :
[mysqld]
ndbcluster
character-set-server=utf8

[mysql]
default-character-set=utf8

[mysql-cluster]
ndb_connectstring=AAA.AAA.AAA.AAA

[ndb_mgmd]
config_file=/var/lib/mysql-cluster/config.ini


【手順4 MySQL Cluster の起動(A, B)】
ここまでの作業で MySQL Cluster が起動できるようになっています。ただ MySQL Cluster を起動する際の注意点として、起動する時の順序は必ず以下の順番で行う必要があります。また停止は逆の順序で行う必要があります:
(1) 管理ノード起動(A)
(2) データノード起動(A, B)
(3) SQL ノード起動(A, B)

具体的には以下の様なオペレーションで MySQL Cluster を起動します:
管理ノード起動(A)
# cd /var/lib/mysql-cluster # ndb_mgmd
(起動メッセージが表示される。管理ノードがSQLノードを兼ねている場合は警告が表示されるが無視)

データノード起動(A, B)
# ndbd

SQLノード起動(A, B) # /etc/init.d/mysql start

この段階でサーバー A, B 双方で MySQL サーバーが起動して、それらはクラスタリング構成になっています。そのことを管理ノード A から ndb_mgm を実行後のプロンプトから show コマンドで確認してみます:
# ndb_mgm
-- NDB Cluster -- Management Client --
ndb_mgm> show
Connected to Management Server at: AAA.AAA.AAA.AAA:1186 管理ノード
Cluster Configuration
 ---------------------
[ndbd(NDB)]     2 node(s)
id=2    @AAA.AAA.AAA.AAA  (mysql-5.6.17 ndb-7.3.5, Nodegroup: 0, *) データノード(マスター)
id=3    @BBB.BBB.BBB.BBB  (mysql-5.6.17 ndb-7.3.5, Nodegroup: 0) データノード

[ndb_mgmd(MGM)] 1 node(s)
id=1    @AAA.AAA.AAA.AAA  (mysql-5.6.17 ndb-7.3.5) 管理ノード

[mysqld(API)]   2 node(s)
id=4    @AAA.AAA.AAA.AAA  (mysql-5.6.17 ndb-7.3.5) SQLノード
id=5    @BBB.BBB.BBB.BBB  (mysql-5.6.17 ndb-7.3.5) SQLノード

ndb_mgm>

こんな感じになっていれば正しくクラスタリングできています。


【手順5 MySQL ユーザーやデータベースの設定(A, B)】
MySQL Cluster では対象のデータベースの同期が取られますが、同期が取られないデータもあります。それらは個別に生成/設定する必要があります:
root ユーザーパスワード設定(A, B)
# mysql -u root
mysql> set password for root@localhost=PASSWORD('XXXXXXXX');

データベース作成(A) これは B に同期される
mysql> create database testdb default character set utf8;

データベースユーザー作成(A, B)
mysql> grant all privileges on testdb.* to test_user@localhost identified by 'test_pass'; 

そして改めて A で作成したデータベース testdb が B に同期されていることを確認します:
test_user ユーザーで testdb にログイン(B)
# mysql -u test_user -ptest_pass testdb
mysql> Bでは作成していないデータベースにログインできた!

※B での mysql コマンドでソケットファイルのエラーが表示される場合は
# mysql -u test_user -ptest_pass testdb --socket=/tmp/mysql.sock
といった感じで、ソケットファイルのパスを指定して実行する必要があります。


【手順6 データの同期を確認(A, B)】
最後に A で作成したテーブルやデータが B に同期されることを確認します。まずはクラスタ対応のテーブルを作成します:
(サーバー A で作業)
# mysql -u test_user -ptest_pass testdb
mysql> create table testtbl(n int) engine=ndbcluster; mysql> show table status *************************** 1. row *************************** Name: testtbl Engine: ndbcluster Version: 10 : :
mysql>

このテーブルに適当にデータを入れておきます:
(サーバー A で作業)
mysql> insert into testtbl values (123); mysql> select * from testtbl; +------+ | n | +------+ | 123 | +------+

このテーブルとデータがサーバー B にも同期されていることを確認します:
(サーバー B で作業)
# mysql -u test_user -ptest_pass testdb (--socket=/tmp/mysql.sock) mysql> select * from testtbl; +------+ | n | +------+ | 123 | +------+

無事、データの同期もできていることが確認できました。





このページのトップヘ