Dockerを使ってElixirの分散システムを気軽に楽しむ

[分散Erlangシステム][Distributed Erlang]を楽しむには複数のPCがあるといいのですが、それだと準備が大変で敷居を高く感じるかもしれません。

[Docker]を使えば一つのPC上に複数の仮想ホストを簡単に立ち上げられます。それらを別々のPCに見立てれば、気軽に遊べるのではないでしょうか。

https://elixirschool.com/ja/lessons/advanced/otp_distribution

https://hexdocs.pm/elixir/Node.html

https://qiita.com/mnishiguchi/items/4d92c839d1ec52a8fd2a　


## やりたいこと

- [Docker]で仮想[Linux]マシンを三つ起動してそれらを別々のPCと見立てる
- それぞれの仮想[Linux]マシンでノードを起動する
- すべてのノードを接続
- 後は自由に遊ぶ

## ノードとは

[分散Erlangのドキュメント][Distributed Erlang]によると

> A distributed Erlang system consists of a number of Erlang runtime systems communicating with each other. Each such runtime system is called a node.

- [分散Erlangシステム][Distributed Erlang]は複数の[Erlangランタイムシステム][Erlang Runtime System]がお互いに通信することで構成される。
- [分散Erlangシステム][Distributed Erlang]上の各ランタイムシステムはノードと呼ばれる。

論よりRun
:speaking_head: < :computer:

## [Docker]でノードを起動する方法

### [Docker]で仮想マシンを起動

まずは[hexpm/elixir]の[Docker]イメージを使用して[Elixir]が使える仮想[Linux]マシンを立ち上げます。

```sh:CMD
docker run --rm -it hexpm/elixir:1.14.2-erlang-25.1.2-alpine-3.16.2 /bin/sh
```

https://hub.docker.com/layers/hexpm/elixir/1.14.2-erlang-25.1.2-alpine-3.16.2/images/sha256-e4ac69be67e32a53eed543ee761296187e94fa9f0e90c238ec56d15e9c562830

### 仮想マシンのIPアドレスを探す

他の仮想マシンと接続するにはIPアドレスが必要になります。

LinuxにはいくつかIPアドレスを探す方法があるようです。

`hostname`、`ifconfig`、`ip`が挙げられます。

```sh:CMD
hostname -i
```

```sh:CMD
ifconfig eth0
```

```sh:CMD
ip -o address
```

```sh:CMD
ip addr show eth0
```

```sh:CMD
cat /etc/hosts
```

余談ですが、ついでにLinuxコマンドで文字列を加工する技を学ぶよい機会でした。

```sh:CMD
ifconfig eth0 | awk '/inet/ {print $2}' | sed -r 's/addr://g'
```

```sh:CMD
ip -o address | awk '/eth0/ {print $4}' | cut -d '/' -f 1
```

```sh:CMD
ip addr show eth0 | awk '/inet/ {print $2}' | cut -d '/' -f 1
```

```sh:CMD
cat /etc/hosts | grep $(hostname) | cut -f 1
```

### ノードを起動

[IEx]を起動するときに`--name`オプションにノードの場所を含む完全修飾名を指定します。IPアドレスはご自身のものに読み替えてください。

```sh:CMD
iex --name hoge@172.17.0.4
```

以上の作業を別々の[シェル][コマンドラインインタプリタ]から実行し、別々の仮想マシンを立ち上げ、ノードを起動します。

![Dockerでノードを起動.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/82804/b36d73e1-b27c-d2b5-11de-c324f9098d28.png)


## [分散Erlangシステム][Distributed Erlang]を構築する

### [Erlangクッキー][Node.set_cookie/2]を設定

ノードが接続する際に必要になります。あるクラスターに属するすべてのノードが同じクッキーを共有することになります。

```elixir:各ノードのIEx
Node.set_cookie(:mycookie)
```

ちなみに`iex`コマンドの`--cookie`オプションまたは`--erl`オプションを用いることにより、[Erlangランタイムシステム][Erlang Runtime System]起動時にクッキーを設定することも可能です。

```sh:CMD
iex --name hoge@172.17.0.4 --cookie mycookie
```

https://github.com/elixir-lang/elixir/blob/950bcc070475bedf70b83a0c6da54b804c350ba1/bin/elixir#L37

```sh:CMD
iex --name hoge@172.17.0.4 --erl "-setcookie mycookie"
```

https://github.com/elixir-lang/elixir/blob/950bcc070475bedf70b83a0c6da54b804c350ba1/bin/elixir#L22

`bin/iex`実行ファイルの一番下の行で`elixir`コマンドが`iex`モードで実行されています。

https://github.com/elixir-lang/elixir/blob/950bcc070475bedf70b83a0c6da54b804c350ba1/bin/iex

https://github.com/elixir-lang/elixir/blob/950bcc070475bedf70b83a0c6da54b804c350ba1/bin/elixir#L107-L110

`erl`のオプションは[Erlang]のドキュメントに列挙されています。

https://www.erlang.org/doc/man/erl.html

### [Node.ping/1]でノードを接続

`hoge`ノードの[IEx]から他のノード（`fuga`ノードと`piyo`ノード）に接続します。

```elixir:hogeノードのIEx
:pong = Node.ping(:"fuga@172.17.0.6")
:pong = Node.ping(:"piyo@172.17.0.7")
```

[Node.ping/1]は、成功した場合は `:pong`、失敗した場合は`:pang`を返します。IPアドレスはご自身のものに置き換えてください。

[Node.list/0]で現在接続されているノードのリストが確認できます。この時点で全てのノードが他の全てのノードに接続されているはずです。

## 後は自由に遊ぶ

### 別ノードでコードを実行

`fuga`ノードのIExに`Toukon`モジュールを貼り付けて定義します。

```elixir:fugaノードのIExに貼り付け
defmodule Toukon do
  def aisatsu, do: "元氣ですかーーーーッ！"
end
```

[Node.spawn/2]を使って、`piyo`ノードのIExから`fuga`ノードで定義された`Toukon`モジュールを実行します。

```elixir:piyoノードのIEx
Node.spawn(
  :"fuga@localhost",
  fn ->
    Toukon.aisatsu() |> IO.puts()
  end
)
```

グループリーダーという概念があるようです。`piyo`ノードが`Node.spawn`を実行した場合は`piyo`ノードがグループリーダーなので結果は`piyo`ノードの標準出力に印字されます。

![別ノードでコードを実行](https://user-images.githubusercontent.com/7563926/204111409-3229b5e7-67d8-4e00-a442-2ce3f6e4a6d4.png)

[Node.spawn/2]以外にも別ノードでコードを実行する方法はいくつか挙げられます。

https://qiita.com/mnishiguchi/items/e8018b7f981472d2fbf7

https://github.com/phoenixframework/phoenix_pubsub

https://www.erlang.org/doc/man/rpc.html

https://www.erlang.org/doc/man/pg.html

今回つかったDockerコンテナを実際のPCやIoTデバイスに置き換えても同様のことができるはずです。

## トラブルシューティング

### [Node.spawn/2]でエラーが出る

これは、ローカルのElixirとリモートのElixirのバージョンが異なるためと思われます。同じバージョンであることをご確認ください。

```sh:エラー
** (BadFunctionError) function #Function<43.3316493/0 in :erl_eval> is invalid, likely because it points to an old version of the code
    :erlang.apply/2
```

[Erlang]のドキュメントに分散Erlangの互換性について以下のように記述されています。

> Erlang nodes can communicate across at least two preceding and two subsequent releases.

前後の2リリースのノード間の通信の互換性は保証されているようです。

https://www.erlang.org/doc/system_principles/misc.html

https://www.erlang.org/downloads

### [Node.start/3]でエラーが出る

本記事では使用していませんが、[Node.start/3]を使用してエラーが出る場合は@zacky1972さんの記事が参考になります。

https://qiita.com/zacky1972/items/cb3aa801a179322c56d6

また、[Livebook]の実装から着想が得られるかもしれません。

https://github.com/livebook-dev/livebook/blob/3a085a5d304c9b44138db76bf4b129675d976c12/lib/livebook/application.ex#L80-L111


## もっとDockerでElixirを楽しみたい

### ローカルのファイルを使ってDockerコンテナの中のElixirに計算させたい

開発PCにあるファイルを使って、Dockerコンテナの中のElixirで計算をしたい場合はDockerボリュームの設定（`docker run`の`-v`オプション）が必要となります。

@torifukukaiouさんの記事が参考になります。

https://qiita.com/torifukukaiou/items/17d55cf896c24b13350e

https://qiita.com/torifukukaiou/items/4f4a250df934f7fa8fe7

### docker-composeを使ってしっかりElixir学習用環境を構築したい

@RyoWakabayashiさんのサンプルコードが参考になります。

https://github.com/RyoWakabayashi/elixir-learning

https://qiita.com/RyoWakabayashi/items/fdc0efa99f35ffb0829f

https://qiita.com/RyoWakabayashi/items/ef858baebecf84028a14

### docker-composeを使って[Phoenix]のウエブアプリを開発環境を構築したい

@koyo-miyamuraさんがサンプルアプリを公開されています。

https://github.com/koyo-miyamura/elixir_phoenix_docker

https://qiita.com/koyo-miyamura/items/a609de2e9fadaf198243

https://qiita.com/koyo-miyamura/items/272b751fbf58755818f6

## ご参考までに

https://speakerdeck.com/elijo/elixirkomiyunitei-falsebu-kifang-guo-nei-onrainbian

https://qiita.com/piacerex/items/e0b6e46b1325bb931122

https://qiita.com/torifukukaiou/items/1edb3e961acf002478fd

https://qiita.com/piacerex/items/e5590fa287d3c89eeebf


[Elixir]: https://elixir-lang.org/
[Erlang]: https://www.erlang.org/
[Phoenix]: https://www.phoenixframework.org/
[Nerves]: https://hexdocs.pm/nerves
[Livebook]: https://livebook.dev/
[IEx]: https://elixirschool.com/ja/lessons/basics/basics/#%E5%AF%BE%E8%A9%B1%E3%83%A2%E3%83%BC%E3%83%89
[Node | hexdocs]: https://hexdocs.pm/elixir/Node.html
[otp_distribution | elixirschool]: https://elixirschool.com/ja/lessons/advanced/otp_distribution
[Node.ping/1]: https://hexdocs.pm/elixir/Node.html#ping/1
[Node.connect/1]: https://hexdocs.pm/elixir/Node.html#connect/1
[Node.spawn/2]: https://hexdocs.pm/elixir/Node.html#spawn/2
[Node.list/0]: https://hexdocs.pm/elixir/Node.html#list/0
[Node.set_cookie/2]: https://hexdocs.pm/elixir/Node.html#set_cookie/2
[Node.get_cookie/0]: https://hexdocs.pm/elixir/Node.html#get_cookie/0
[Node.start/3]: https://hexdocs.pm/elixir/Node.html#start/3
[epmd]: https://www.erlang.org/doc/man/epmd.html
[Elixirの分散処理(Node)とMnesia]: https://qiita.com/sand/items/c5c9dab1dce3d5c526ee
[Elixir Distribution, ELI5]: https://medium.com/@pawel_dawczak/elixir-distribution-eli5-309193004119
[Node.start/3でエラーが出る場合にはepmdをバックグラウンド起動する]: https://qiita.com/zacky1972/items/cb3aa801a179322c56d6
[コマンドラインインタプリタ]: https://ja.wikipedia.org/wiki/%E3%82%B3%E3%83%9E%E3%83%B3%E3%83%89%E3%83%A9%E3%82%A4%E3%83%B3%E3%82%A4%E3%83%B3%E3%82%BF%E3%83%97%E3%83%AA%E3%82%BF
[端末エミュレータ]: https://ja.wikipedia.org/wiki/%E7%AB%AF%E6%9C%AB%E3%82%A8%E3%83%9F%E3%83%A5%E3%83%AC%E3%83%BC%E3%82%BF
[メッシュネットワーク]: https://ja.wikipedia.org/wiki/%E3%83%A1%E3%83%83%E3%82%B7%E3%83%A5%E3%83%8D%E3%83%83%E3%83%88%E3%83%AF%E3%83%BC%E3%82%AF
[Distributed Erlang]: https://www.erlang.org/doc/reference_manual/distributed.html
[ホスト名]: https://ja.wikipedia.org/wiki/%E3%83%9B%E3%82%B9%E3%83%88%E5%90%8D
[hexpm/elixir]: https://hub.docker.com/r/hexpm/elixir
[Distributed Erlang]: https://www.erlang.org/doc/reference_manual/distributed.html
[Erlang Runtime System]: https://www.erlang.org/doc/system_architecture_intro/sys_arch_intro#erlang-runtime-system
[Linux]: https://ja.wikipedia.org/wiki/Linux
[Docker]: https://ja.wikipedia.org/wiki/Docker