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

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

タグ:raspberry

日本での正式な発売直後に品切れになり、しばらく入手できなかったラズベリーパイ 400 (以下「ラズパイ 400」)の販売が再開されたようです:
品切れだったラズパイ内蔵キーボード「Raspberry Pi 400」日本語版、販売を再開 9790円



幸いなことに自分はこれ(日本語キーボード版)を入手することができていました。そして罰当たりなことに、このラズパイ 400 を「起動したまま SSH で使う」という使い方をしていました。SSH 接続ならなにもラズパイ 400 じゃなくて普通のラズパイでも・・・という声が聞こえてきそうです。ごめんなさい、その通りだと思いつつ使ってました。

言い訳をするつもりはないのですが、ラズパイ 400 はキーボード本体内にラズパイ4(4GBモデル)が内蔵されているもので、ここに電源とモニター(と現実的にはマウス)をつないで使うことになります。マウスは Bluetooth でもよいので物理的な接続は必須ではなく、(内蔵バッテリーがないため)電源をつながないといけないのは仕方ないとして、モニターもつながないと実質的に使えないというのが普段使いにはちょっとしたハンディキャップになっていました。本体とキーボードのセットと考えるとかなり小型化されていて持ち運びに便利な一方で、モニターも HDMI ケーブルごと持ち歩かないといけないのだとするとラズパイ 400 の機動性はかなり落ちてしまいます。モニターのある部屋からモニターのある部屋へ移動して使うにはいいんだけど、それだと利用シーンはかなり限られてしまうような気もしてしまいます。。

で、今回のブログのテーマとなります。そんなラズパイ 400 ですが、普段から持ち歩いているスマートフォン(スマホ)をモニター代わりに使うことができるのであれば、実質的にモニターを持ち歩いているようなものなので、上述の課題を解決できるのではないかと考えました。これは実現性を無視した都合のよい話ではなく、HDMI input をサポートした Android 11 以降であれば理論上はできるはず、、ということで試してみたのでした。ちなみに iOS だとまだこの機能はサポートされていない模様です。


【システム構成】
今回試したシステム構成はこのような形です:
2021091901


通常、ラズパイとディスプレイとを HDMI ケーブルで接続するのですが、そのディスプレイの代わりにスマートフォンを使うことになります。その際に HDMI ケーブルをスマートフォンの端子(USB Type-C)にアダプタで直接変換すればよいわけではなく、間にキャプチャーカードと呼ばれる機器を挟む必要があります。今回は以下の「BANANAJOY ビデオキャプチャカード」を使いました:



また、このキャプチャー結果の信号を USB Type-C 端子を持つスマホに送るための変換ケーブルとしては以下の「MacLab. USB Type-C OTG 変換 ケーブル」を使いました:



加えて、スマホ側にも単に信号を送っただけではディスプレイとして表示されないので、「スマホを外部ディスプレイ化するアプリ」が必要です。今回はこの「USB Camera」アプリをダウンロードして使いました。なおこのアプリは無料ですが、有料版(USB Camera Pro)だと広告が表示されないだけでなく、画面サイズを変更できるなどの追加機能が使えるようです:
USB Camera - Connect EasyCap or USB WebCam


ちなみに今回の実験で使ったスマホはこの Unihertz Titan Pocket です。この機種である必要はありませんが、Android 11 が搭載されている必要があると思っています:
QWERTYキー付き小型スマホ、Unihertz「Titan Pocket」が到着!

※あと今回のレビューでは使っていませんが、この機種だと標準アプリとして「USBカメラ」というものが含まれていて、これで外部ディスプレイ化できることも確認できました。


で、上述のシステム構成のようにラズパイ 400 、HDMI ケーブル、キャプチャーカード、OTG 変換ケーブル、そしてスマホを接続し、スマホにインストールした USB Camera アプリを起動した状態でラズパイ 400 に電源を入れると・・・
IMG_3924


↑ラズパイ 400 が起動し、その画面がスマホ内に表示されました!期待通り!!

↓ブラウザとターミナルを開いて使っている時の画面の様子がこちらです:
IMG_3925


あっけなく実現できてしまいました。今回スマホとして使った Unihertz Titan Pocket は決して広い画面を持っているわけではないので、実用性という観点ではあまりよくないかも(苦笑)しれません。まあ、でも普通の画面(?)の Android 11 を所有していて、有料版の USB カメラアプリで画面をポートレートモード(横)固定で使えばもう少し見やすくなりそうだと思ってます。

※ここでスマホに表示される画面はあくまで「外部ディスプレイ化されたスマホに単に表示されている画面」です。タッチ操作に対応はしていません。タッチしても何の反応もありません。


さて、ということはラズパイ 400 を持ち歩いて使う上で、かなり進展があったと思っています。何しろ「普段から外部ディスプレイ(=スマホ)を持ち歩いていた」ことになるので、接続用のケーブル類だけ持ち歩いていればどこでもラズパイを使える、ということになります!!

その接続用に持ち歩くケーブル類ですが、具体的には・・・
・電源ケーブル(USB-C ケーブルと、コンセントにつなぐ電源アウトレット)
・Micro HDMI -> HDMI 変換アダプタ
・HDMI ケーブル
・HDMI キャプチャーカード
・USB Type-C OTG 変換ケーブル
・外付けマウス(有線か、または Bluetooth)


・・・んー、意外と多いな(汗) (^^; それと結局バッテリーがないからコンセントかモバイルバッテリーに繋いで使う必要があるんですよね。これが次のネックになりそうな気がしてきました。。

※なお、HDMI キャプチャーカードが利用する電源は出力側(スマホ側)から使われる模様です。つまりこの仕組を使っている間、スマホ側のバッテリーは想定以上に消費されることになる点に注意してください。


ともあれ、Android 11 が使えるユーザーにとってはフルキーボード付きの Debian Linux 環境を持ち歩いて使える手段の目途が立つ実験結果でした。


元ネタはこの記事です:
Cloudflare の ngrok的なサービスがあるらしい ⇒ $ cloudflared tunnel --url localhost:8080 といった使い方

(特に開発中のサービスなど)ローカルホスト内で動いている状態のウェブサービスを試験的・一時的にインターネットに公開して外部から利用できるようにするものです。この手のツールとしては ngrok が有名ですが、CDN で有名な Cloudflare 社も同様のツールを公開していたんですね。。

上記ページはその導入手順を含めて紹介しています。また公式のインストールページもリンクされています:
https://developers.cloudflare.com/cloudflare-one/connections/connect-apps/install-and-setup/installation


が、残念ながらラズベリーパイ向けのインストール方法はここで書かれている方法だけではわかりにくい内容でした(要はラズパイの場合、ソースコードからビルドする必要があるのですが、ビルドの前提となる Go 言語のインストールがまた面倒で・・・)。というわけで、ラズベリーパイ環境向けの cloudflared のインストール手順を以下に紹介します。


【ラズベリーパイ向け cloudflared のインストール手順】
(1) Go 言語のインストール

この後の手順で Go 言語を使って cloudflared をビルドします。そのため Go 言語をインストールしておく必要があります。

以前に紹介した以下のサイトを参照して、ラズベリーパイに Go 言語をインストールしてください:
ラズベリーパイに Go 言語をインストールする


(2) cloudflared のビルド

Go 言語用のソースコードをダウンロードして、Go 言語でビルドしてツールを作成します。

まず GitHub からソースコードをダウンロードします:
$ git clone https://github.com/cloudflare/cloudflared.git

ダウンロードしたソースコードを Go 言語でビルド&インストールします:
$ cd cloudflared

$ make cloudflared

$ go install github.com/cloudflare/cloudflared/cmd/cloudflared

ここまでの手順で ~/go/bin/cloudflared に実行バイナリが作られています。このままだと利用が不便なので、パスの通ったフォルダ(例えば /usr/local/bin/ 以下)にこのファイルをコピーします:
$ sudo cp ~/go/bin/cloudclared /usr/local/bin/

これでインストール完了です。


【ラズベリーパイで cloudflared を使ってみる】

ではインストールした cloudflared を実際に使ってみます。そのためには何でもいいのですが、何らかのウェブアプリケーションをラズベリーパイ上で動かす必要があります。docker で nginx を動かすとか、適当なウェブアプリを動かすとかでも構いません。こちらの方法を参照いただいて Node-RED をインストールして動かす、とかでも構いません(以下は最後の方法を実行し、 $ node-red コマンドまでを実行して、Node-RED が 1880 番ポートで稼働しているという前提で説明を続けます):
2021040801
(↑プライベートアドレスの 1880 番ポートで待受け準備完了した様子)


ラズベリーパイ上で何かウェブアプリが動いていたら、ラズベリーパイの端末(既に利用中の場合は別のターミナルの画面)から cloudflared コマンドを実行します:
$ cloudflared tunnel --url localhost:1880

※上述コマンドの 1880 部分は実際にリクエストの待受けをしているポート番号を指定します

すると実行結果の画面に以下のようなメッセージが表示されます:
  :
  :
2021-04-08T05:01:22Z INF +-------------------------------------------------------+
2021-04-08T05:01:22Z INF |  Your free tunnel has started! Visit it:              |
2021-04-08T05:01:22Z INF |    https://xxxxxx-xx-xxxxxx-xxxxxx.trycloudflare.com  |
2021-04-08T05:01:22Z INF +-------------------------------------------------------+
  :
  :

この青字で表示された URL が cloudflared のトンネリングによってインターネットに公開されたアドレスです。自分の PC はもちろん、全く別のネットワークに接続された別の PC からでもウェブブラウザでこのアドレスにアクセスすると、ラズベリーパイの localhost:1880 で稼働しているアプリケーションをパブリックなインターネットから見ることができるようになります:
2021040802
(↑パブリックアドレスで https で待受け準備完了した様子)



ラズベリーパイへの Go 言語インストール、普通にリポジトリから $ sudo apt-get install go でインストールできるかと思っていたら出来なかったので手順を調べました:
go


2021/04/07 現在ではラズベリーパイに Go 言語をインストールするには最新版バイナリ(のアーカイブ)をダウンロードして展開するのがてっとり早そうでした。

まずはこのページで Go 言語の最新バージョンを確認します:
https://golang.org/dl/

2021040701


2021/04/07 時点での最新版安定(stable)バージョンは 1.16.3 のようでした。以下、このバージョンをインストールする前提で説明を続けます。

ラズベリーパイのターミナルから、ラズパイアーキテクチャ(armv6l)向け最新バイナリをダウンロードして、/usr/local 以下に展開します:
$ wget https://golang.org/dl/go1.16.3.linux-armv6l.tar.gz

$ sudo tar -C /usr/local -xzf go1.16.3.linux-armv6l.tar.gz

これで /usr/local/go フォルダ以下に Go 言語がコピーされました。実際の go コマンドは /usr/local/go/bin/go に存在している状態です。

このままだとパスが通っていなくて不便なので、パスを通します。~/.bashrc ファイルをテキストエディタで開いて、最終行に以下を追加して保存します:
export PATH=$PATH:/usr/local/go/bin

保存後、 $ source ~/.bashrc を実行するか、ターミナルを一度終了して開き直すと設定が有効になります。go version コマンドを実行して、以下のような結果が表示されればインストール成功です:
$ go version
go version go1.16.3 linux/arm

 

ラズベリーパイに Raspberry Pi OS をインストールする場合、多くのケースで Lite 版以外のデスクトップ版やサードパーティアプリまで含まれた拡張デスクトップ版を使うと思っています。この2つであれば初期セットアップの段階からデスクトップ GUI による操作が可能となり、無線LANの設定含めて便利に環境構築ができます。逆に Lite 版を選んだ場合は初期段階では CUI による操作が必要になり、ある程度まではキーボードによるコマンド実行や環境構築が必要になります(正しいキーボード設定をするまではキーボードレイアウトも合っていない可能性もあります)。慣れないと不便ですが、一方で余計なものが含まれていない状態であるとも言え、非常に軽量で動作する環境でもあります。特にラズベリーパイゼロ(以降「ラズパイゼロ」)のようにリソースの限られたハードウェアを使おうとすると、デスクトップ版だと起動の時点でリソースの多くが使われてしまい、初期セットアップ作業もかなり時間をかけて行う必要がでてきてしまいます。そんなラズパイゼロでは作業効率を考えると Lite 版で初期セットアップを行い、必要に応じて後からデスクトップ環境も導入する、という使い方も考慮する必要があります。

さて、この面倒な状況をなんとかならんか・・・と考え、短絡的に「デスクトップ環境をもっと軽量にできないか?」と試みることにしました。自分は 30 年近く UNIX を使っていて、自分が使い始めた当初は "X Window" なるウィンドウシステムが(正確には X11 が)使われ始めていた、という時期でもありました。当時の自分は今ほど CUI に理解はなく(苦笑)、GUI オペレーションが可能な X Window を使いたくて個人の環境でも試行錯誤していた時期でした。

※より正確には当時の Linux(Slackware) でもインストール時に X Window を含める選択をすることは可能でした。が、標準機能で導入される X Window は twm(Tab Window manager) と呼ばれる、見た目も操作感もイマイチ・・・な GUI で、「これじゃなくて大学の研究室で使ってるやつを使いたい」と試行錯誤していたのでした。その「大学の研究室で使ってるやつ」は現在ではオープン化された mwm(Motif Window Manager) で、当時は無料ではなかったと記憶しています。事実、僕はこれを買ってまで使っていました(メディアもまだ持ってます)。思い入れのあるウィンドウマネージャーです:
IMG_3176


話を戻すと、要は「ラズパイ(ゼロ)でも X Window だけを入れれば軽量 GUI が実現できるのではないか?」&「今なら mwm も無料で使えるので(折角なので) mwm で使いたい」と考えて、「ラズパイで X Window + mwm を使う方法」を調べたのでこのブログで共有します。なお以下の内容はラズパイゼロでも使えることを確認していますが、普通のラズパイでも動く内容です(普通のラズパイなら普通のデスクトップでもそんなに苦労はしないと思うけど)。


【ラズパイ(ゼロ)に X Window と mwm をインストールする】
まずラズパイ(ゼロ)を Raspberry Pi OSLite 版でセットアップします。ある意味、ここが一番大変だと思ってます。ここに書かれた内容を参照するなどして初期セットアップまでを終えておいてください(以下の手順では SSH 接続やシリアルコンソール接続は必須ではありません):
Raspbian Liteの初期設定 令和2年(2020年)3月版

改めて X Window と mwm を利用する上で必要なツール類を追加インストールします。必須ではありませんが、せっかく GUI を使うならウェブブラウザくらいは・・と思って最後に Chromium(chromium-browser) も含めていますが、不要であれば指定しなくても構いません:
$ sudo apt-get install libxm4 mwm xserver-xorg xinit x11-xserver-utils xterm x11-apps chromium-browser

ツール類のインストールが完了したらホームディレクトリに .xinitrc という名前のファイルを作成します。X Window 起動時の各種設定をするファイルで自分はとりあえず以下の内容にしました(上記のツール類をすべて導入していれば起動エラーは起こらないと思います):
#!/bin/sh

xset s off
xset -dpms
xset s noblank

xsetroot -solid darkslateblue

xeyes -geometry 70x70+5+5 &
xclock -geometry 70x70+105+5 &
xterm -geometry 80x20+100+100 &
exec mwm

これで準備は完了。最後に以下のコマンドを実行して X Window を起動します:
$ startx

成功するとこのような画面が表示されます(この画面で「懐かしい!」と感じる人はアラフィフ以上のオッサンだと思います(笑))。普段見慣れた GUI と比べるとかなりシンプルに感じますが、xterm のタイトル部分でラズパイ環境であることもわかります:
1

※x11-apps パッケージを導入しているので、xeyes や xclocks 以外に以下のページで紹介されているアプリも使えます:
https://packages.debian.org/stretch/x11-apps


せっかくなので導入した Chromium も起動してみます。xterm の画面から起動コマンドを最後に & を付けるのを忘れずに実行すると、指定したアプリが  X Window 内で起動し、xterm も(実行したままにならず)プロンプトが戻って続けてコマンドを実行できる状態になります:
$ chromium-browser &

2


自分の感想ですが、ラズパイゼロだと Chromium を起動すると「さすがにちと厳しいかも・・」という印象の操作感になります(現実的には日本語フォントなども追加でインストールすべきだし・・)。ただ通常のデスクトップ GUI で起動した時と比べると全体的にまだ全然軽いですね。


ちなみに X Window を終了するには背景部分を右クリックして、ポップアップメニューから "Quit" を選択、です。
3


4


昔はこの mwm って有料だったんだよなあ。その頃を知っているので、オープン化されて無料で使えるようになった mwm が簡単に使えるようになったのは改めて感慨深いものがあります。

また、.xinitrc ファイルの最後の "exec mwm" 部分で、(足りない場合は apt-get で導入してから)mwm 以外の別のウィンドウマネージャーを指定することもできます。有名どころはデフォルトだった twm や fvwm2 あたりでしょうかね。まあ使い慣れたものがあったら、それを使うのがいいと思っていますが、僕は「買ってでも欲しかった」mwm 一択です。



以前にラズパイ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



このページのトップヘ