ブルーアイズで理解するDocker SwarmとそのDockerfile
前の記事ではとりあえず動かしてみようでDocker Swarmを使ってみましたが、今回はその理解編です。
主にSwarm環境を作るときのDockerfile について理解を深めたいと思います。
調べるなかで「これ遊戯王のブルーアイズホワイトドラゴンじゃん」と思ったので、それで例えます。
Swarmで管理することは「ブルーアイズ三体連結」
Swarmによるクラスタ管理はブルーアイズ三体連結で例える事ができます。

3体のブルーアイズは鎖で繋がっており、連続攻撃ができます。
これは決して融合モンスター(アルティメットドラゴン)ではありません。たしかに寄せ集まって一体のモンスター風には見えますが、ブルーアイズが鎖で繋がってるだけで個々で動いてます。
だからアルティメットバーストを撃つのではなく、滅びのバーストストリーム × 3の連続攻撃をします[1]。
Swarmで管理する際も、大きな一つのコンテナが動いてるいるのではなく、SSHで繋がった全く同じコンテナが同時に動いています。そしてそれが見かけ上、一つのコンテナ風に振る舞っています。実態はバラバラです。
細かく理解
もう少し解説します。主にコンテナとSSHについて詳しく話します。
コンテナ = ブルーアイズ
Docker Swarm は全く同じコンテナを管理します。
遊戯王の「同名のカード」です。
ブルーアイズホワイトドラゴンという同名モンスターが3体いると思って下さい。決して融合はされていません。ただ同じモンスターが3体並んでいるだけです。
SSH = 鎖
これらブルーアイズたちは鎖で繋がれ連結しているわけですが、この鎖も一本一本で全く同じものが使われています。
クラスタでもコンテナたちはSSHで繋がれています。このとき、SSH認証鍵はすべてのコンテナで共通したものが使用されます。
通常SSHではノードA → ノードB で接続する場合、ユーザー側(ノードA)で使う公開鍵と秘密鍵、サーバー(ノードB)で使う公開鍵と秘密鍵の2ペアが必要です。
B → A でもSSH接続できるようにするなら、さらに2ペアの鍵が必要になります。
クラスタでは全てのノードがユーザーとサーバーにどちらにもなり得るのため、必要な鍵の数がノードの数に応じて指数的に増加してしまいます。
これは大変なので、クラスタでは全てのコンテナでユーザー用の鍵(公開鍵と秘密鍵のペア)とホスト用の鍵(公開鍵と秘密鍵のペア)を共通させます。

真ん中の顔面はsshd
ブルーアイズは魔法カード「邪悪なる鎖」によって束ねられています。
このカードの真ん中に顔があります。これはsshdです笑
SSHを常に見張ってる人です。デーモンなので休まずずっと働いています。だからこんな険しい顔をしています。
大変そうです。

Swarmでは連結した集まりを手続き上一つにできる
結局Swarmでの管理とはなんなのかというと、連結したカードたちを一括操作できることです。
実態はブルーアイズが連なってるだけですが、手続き上は一枚のカードとして扱えたりします。
例えば、巨大化のカードを発動してブルーアイズ三体連結に装備したら、鎖に繋がってるブルーアイズはみんなパワーアップします。
ただし、攻撃するときはブルーアイズ各々が別の相手モンスターにバーストストリームを打てます(攻撃できます)。
Swarmで管理していれば、一括で全てのコンテナをデプロイ(遊戯王でいう召喚)、処理の実行(攻撃)、停止(手札に戻す)などを命令できます。しかし繰り返しますが、実態は複数の同じコンテナが動いているに過ぎません。

どんなメリットがある?
安定性の向上
例えば、相手がサンダーブレイクを発動したとします。
アルティメットドラゴンだとそれだけで破壊されてしまいます。
しかし三体連結では、1体のブルーアイズはやられますが、2体は残っているのでまだ戦えます。

例えば、建物に雷が落ちたとします。
複数のノードで一つの大規模プログラムを実行している場合、一つでもノードが落ちたらプログラム全体に影響が出てしまいます。
しかしSwarm管理の場合は一つのノードが落ちても、残りのノードたちでなんとか保持しようとSwarmが色々頑張ってくれます。

容易なスケーリング
仮に遊戯王でブルーアイズの四体融合ができるとして、一度でも三体融合でアルティメットにしてしまうと、四体目を融合するのは大変です。
まず融合解除で元のブルーアイズに戻して、4体目のブルーアイズで再度融合する必要があります。
これは面倒です。
しかし連結しているだけなら、4体目のブルーアイズが召喚されても、鎖で繋げば良いだけなので簡単に強化できます。

例えば、クラスタにノードを追加したいとき、本来なら各ノードはクラスタ内の全てのノードとそのIPを知る必要があり、HOSTファイルなどに追記が必要です。
node1: 192.168.1.10
node2: 192.168.1.11
node3: 192.168.1.12
# node4のIPを追加 この情報を全てのノードで統一
Swarmならこの作業をする必要はありません。
Swarmは「オーバーレイネットワーク」を構築し、そこにいるコンテナ専用のIPを新たに振り分けます[2]。
なので、各ノードはあらかじめ全員のノードを知っている必要はなく、作られたオーバーレイネットワークに入り込めば互いに通信できるようになります。
ちなみに、Wi-FiやLANの上に仮想のネットワークをつくるので「オーバーレイ」です。
(デュエリストにとってはややこしいですが、エクシーズ召喚ではありません)
ロードバランシング
遊戯王ではモンスターは通常1ターンに一度しか攻撃できません。
ブルーアイズ三体連結は8回連続で攻撃できますが、やはりリソースには限りがあります。
なので「このブルーアイズはすでに攻撃してる、別のブルーアイズで攻撃だ!」と、どのブルーアイズを使うかは多少気にしないと行けません。
Swarmも同じようなことをしています。そのとき手の空いているコンテナに外部からのリクエストをパスしてくれます。
Dockerfileで理解
ここまでの解説を踏まえDockerfileを見れば実際にないやっていることがわかりやすいと思います。
全体像はこんな感じ
FROM python:3.9-slim-bookworm
RUN apt-get update && apt-get install -y \
openssh-server openmpi-bin libopenmpi-dev iputils-ping \
&& rm -rf /var/lib/apt/lists/*
RUN mkdir -p /var/run/sshd && ssh-keygen -A
RUN echo 'root:mpi' | chpasswd && \
sed -i 's/#PermitRootLogin prohibit-password/PermitRootLogin yes/' /etc/ssh/sshd_config
RUN mkdir -p /root/.ssh && \
ssh-keygen -t rsa -N "" -f /root/.ssh/id_rsa && \
cat /root/.ssh/id_rsa.pub >> /root/.ssh/authorized_keys && \
echo "StrictHostKeyChecking no" > /root/.ssh/config && \
echo "UserKnownHostsFile /dev/null" >> /root/.ssh/config
RUN chmod 700 /root/.ssh && \
chmod 600 /root/.ssh/id_rsa /root/.ssh/authorized_keys /root/.ssh/config && \
chown -R root:root /root/.ssh
WORKDIR /app
COPY コピーしたいファイルなど /app/
CMD ["/usr/sbin/sshd", "-D", "-e"]
sshdが動くための指示(邪悪なる鎖の顔面の発動)
まずはクラスタで重要なSSHのための準備をします。デーモンが動けるようにディレクトリや鍵を生成します。そして最後の行でデーモンを動かし続ける指示を出します。
CMD ["/usr/sbin/sshd", "-D"]
通常、sshdは起動するとすぐにバックグラウンドに隠れます。
しかし、Dockerコンテナはメインのプログラムが終了すると、コンテナ自体も終了するという性質があります(遊戯王のスピリットモンスターです)。
そこで -D(非デプロイモード)を付けることで、sshdをフォアグラウンド(最前線)で動かし続け、コンテナが終了しないようにしています。
ただし、sshdは起動する際に一時的に情報を/var/run/sshdに書き込もうとします。通常はLinuxのインストール時にOSが作っててくれます。しかし、Dockerで軽量なslim版などのイメージを使うとこのディレクトリが作られていないことがあります。
使うベースイメージによってはDockerfileで作成が必要です。(今回もディレクトリを作る指示を出しています)
なのでこのコマンドも必要です。
RUN mkdir -p /var/run/sshd
クラスタではコンテナは互いにSSH接続しあいます。すなわちすべてのコンテナがSSHのサーバー側になり、ユーザー側にもなります。すなわち、サーバー側での鍵とユーザー側での鍵の両方が必要です。
まずはサーバー側としての鍵をここで作ります。
RUN ssh-keygen -A
# 上のDockerfileでは &&でつなげてます
rootユーザーでログインへ
通常セキュリティの観点からrootでのSSH接続は拒否されています[3]。
ただし、コンテナは仮想的なものなのと、ライブラリがroot前提のものだったりするのでrootで接続できるようにします。
今回rootのパスワードはmpiにしています。
RUN echo 'root:mpi' | chpasswd && \
sed -i 's/#PermitRootLogin prohibit-password/PermitRootLogin yes/' /etc/ssh/sshd_config
ユーザー鍵の生成
サーバー側として振る舞うときの鍵は上で作ったので、今度はユーザー(クライアント)として振る舞うときの鍵を作ります。
RUN mkdir -p /root/.ssh && \
ssh-keygen -t rsa -N "" -f /root/.ssh/id_rsa && \
cat /root/.ssh/id_rsa.pub >> /root/.ssh/authorized_keys && \
echo "StrictHostKeyChecking no" > /root/.ssh/config && \
echo "UserKnownHostsFile /dev/null" >> /root/.ssh/config
まず、鍵を置いておくための隠しディレクトリ/root/.sshを作ります。
そしてssh-keygenでユーザー鍵を生成します。今まで作ったサーバー用の鍵、ユーザー用の鍵は全てのコンテナで共通しています。
停止させない工夫
鍵を生成する際にはコンテナ間での通信が停止しないような工夫がいくつかあります。
まず、鍵を生成する際はパスワードは空にします。「鍵を入れてください」的なやりとりで接続を止めないためです。
また、作った鍵をauthorized_keysに登録して「この鍵を持っている人なら入っていいよ」の許可を得ます。
さらに、通常初回のSSHで別のマシンに繋ぐと、**「この接続先、本当に信用していいですか?(yes/no)」**と聞かれます。これも停止の原因になりるので、スキップします。
さらにさらに、一度使いたいだ相手の情報をknown_hostsに残さないようにもしています。
コンテナは作り直すたびにホスト鍵が変わることがあります。なので、以前の情報が残っているとエラーが出るのを防ぎます。
まとめ
この記事ではSwarmによるクラスタ管理や底で用いられるSSHについてブルーアイズホワイトドラゴンで例えました。
Swarmのクラスタ管理の実体とは連結させて手続き上一つに見せるもので「ブルーアイズ三体連結」のようなもので、コンテナとは「ブルーアイズ」、SSHとは「鎖」でした。
また、Swarmを使うメリットである安定性、スケーリング、ロードバランシングについても紹介しました。
さらに、Dockerfile で何をしているか確認することでSwarm でクラスタの管理をするために必要な設定を説明しました。
デュエリストの皆さまなら少しはピンとくる箇所があれば幸いです。
Discussion