複数の Nerves マシンを通信させる

この記事は、[#NervesJP Advent Calendar 2019](https://qiita.com/advent-calendar/2019/nervesjp) の16日目です。昨日は [@y_jono](http://y-jono.hatenablog.com/about) さんの [NervesでEV3(lego mindstorms)を走行させよう（NervesでのEV3ファームウェア開発）](http://y-jono.hatenablog.com/entry/2019/12/16/001034) でした。

今日は複数の Nerves マシン同士を通信させる方法を考えてみたいと思います。本当にこんなことをしないとならないのか未だに半信半疑なので、もしこんな方法があるよという話があればぜひお知らせください。

# 概要
Nervesに限らず、ElixirでもErlangでも「複数ノードで並行プログラミング」の話はいくつもあります。これは単一マシン・単一OS上での複数ノードのことが多く、「複数マシンでネットワーク的に分散している環境で並行プログラミング」の話はあまり転がっていません。今回はこれについて考えてみました。以下「ノード」というのはElixir用語でのnodeを指すことにします。個体として独立したハードウェアやOSについて「マシン」という名前で統一します。

## 複数マシンでのElixirマシンの接続
異なるマシン上でElixirを稼働してそれらのプロセス同士で通信して動かすことが可能です。このときElixirが以下の条件で稼働していることが必要です。

* ノード名の `@` 以降にマシンを識別するIPアドレスが表記されている
* クッキーが使用されている

例えば iex を起動するなら以下のように起動します。

```sh:aliceのターミナル
$ iex --name alice@192.168.55.3 --cookie comecomeeverybody
```

```sh:bobのターミナル
$ iex --name bob@192.168.55.7 --cookie comecomeeverybody
```

すると `Node.connect/1` 関数 [^nodeconnect] で互いを結びつけることが出来ます。

[^nodeconnect]: おそらくErlangの :net_kernel.connect_node/1 関数へのラッパ。

```exs
iex(alice@192.168.55.3)1> Node.connect(:"bob@192.168.55.7")
true
iex(alice@192.168.55.3)2> Node.list                     
[:"bob@192.168.55.7"]
```

これは Nerves であっても（きっとおそらく）同様です。Nerves を使うときに固定IPアドレスが使える環境ならネットワークインタフェースにアドレスを指定して、ノード名の `@` 以下にアドレスを書いて渡せばできるに違いありません。Nerves でのアドレスの設定の仕方は[Nerves: Connecting to your Nerves target](https://hexdocs.pm/nerves/getting-started.html#connecting-to-your-nerves-target) を御覧ください。


## Virtual Ethernet を使う場合の問題
ただしこの「ノード名の `@` 以下にアドレスを書いて」が曲者です。PCで遊んでいるときは（自由に書ける場合は）良いのです。

しかしながら、Nerves の場合はちょっと違います。ターゲットマシンをUSBでホストPCにつないで、その上でVirtual Ethernetを使うことがほとんどでしょう。このときがちょっと面倒になります。

`@` から後ろが `nerves.local` になってますね。これの `.local` はホストPCからみると mDNS (multicast DNS) で名前解決をしてターゲットのVirtual Ethernet上のIPアドレスを発見するのに使います。なのでここに IP アドレスを持ってくるとホストPCで `ssh nerves.local` と書いてもアクセスできなくなります。USB の口の Virtual Ethernet 側の IP アドレスが解決できなくなるので、ここにはIPアドレスを書けないのです。


## とりあえずサーバを介して接続できれば良しとする
この問題は、例えば `Node.connect/2` 関数があって、第1引数が `node` 第2引数がIPアドレスを指定できたりすると一気に解決なのですが、そういう関数はありません。なので2つのNervesマシンをいきなり接続するのが難しそうです。しょうがないので仲介するサーバがあればよいということに今回は落ち着きました（落ち着かせました）。仮定としてサーバは固定のIPを持つものとします。

### 今回のネットワーク
ラズパイ0Wが2つあって、それぞれ Alice と Bob という名前にします。それぞれ Virtual Ethenet 経由でホストPCにつながってます。また Alice と Bob は WiFi 経由で IP で到達可能なネットワーク上にある Server マシンにつなげることが可能です。

![Nerves.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/260320/fd0b4533-aa0f-ed67-f4a4-00d1ae2dfc48.jpeg "今回仮定するネットワーク")

よくある構図と思います。Nerves トレーニングを受けた方は、トレーニングのセットが2つあると思ってください。トレーニングのときはserverの方には NervesHub がありましたね。

### 基本方針
このネットワークでAliceとBobが間接的にでもつながれば良しとします。すなわち「Alice と Bob は Server と通信できる」こと、さらに言い換えると「それぞれが Server に対して `Node.connect/1` 関数で接続できる」ことが出来れば今回はOKとします。このために以下をします。

* ノード名の `@` より前を変える
 * 全部が *プロジェクト名* @nerves.local だと、server から見て何がどれなんだか区別できないので
* クッキーを設定する
 * クッキーを設定しないと `Node.connect/1` が接続してくれないので

# 設定
では具体的な設定を行っていきます。

## 基本設定
まずは Nerves の基本的な設定をしてください。ラズパイ0Wの USB ケーブルで Virtual Ethernet を使うことを前提にしています。あと WiFi の設定は以下のようにします。

```exs
config :default,
  wlan0: [
    networks: [
      [
        ssid: "お手元のWiFiのSSID",
        psk: "お手元のWiFiのパスワード",
        key_mgmt: :"WPA-PSK" # WPA-PSK2の場合でもこのままで繋がりました
      ]
    ]
  ]
```

あとは以下でターゲットの Nerves マシンを作ります。おそらくこれだけだと思うのですが、色々やってるうちに失念したかもしれません。何かおかしかったら教えて下さいませ。

```sh
$ export MIX_TARGET=rpi0
$ mix deps.get
$ mix firmware
$ mix firmware.get.script
$ ./upload.sh
```

第1行目の環境変数 `MIX_TERGET` はラズパイ0系用にしてあります。マシンの種類によって
[Nerves: Supported Targets and Systems](https://hexdocs.pm/nerves/targets.html#supported-targets-and-systems)
から選んでください。

## ノード名を変える
Nerves というか Elixir というか Erlang のノード名を変更するにはいくつか方法があります。ここに見つけた方法をあげておきます。今回は最初の方法で実施しました。

### rel/vm.args を変更する
Nerves のターゲットの開発環境ディレクトリ（トレーニングで `_target` となるディレクトリ）の下に `rel/vm.args.eex` というファイルがあります。

```erlang
## Add custom options here

## Distributed Erlang Options
##  The cookie needs to be configured prior to vm boot for
##  for read only filesystem.
```

これに以下の行を加えるとノード名が変更できます。`alice` の場合は以下です。

```
-name alice@nerves.local
```

詳しくは [Erlang: vm.args](https://github.com/bitwalker/distillery/blob/master/docs/config/vm.args.md) を読んでください。

### config/target.exs を変更する
Nerves のファイルを変更することでもノード名を変更できます。

```exs
config :nerves_init_gadget,
  ifname: "usb0",
  address_method: :dhcpd,
  mdns_domain: "nerves.local",
#  node_name: node_name,  # これをコメントアウト
  node_name: :alice,      # これを追加
  node_host: :mdns_domain
```

ノード名を変更するだけならこれでも良かったのですが、以下に続くクッキーの変更もあるので、今回はこちらは使いませんでした。

### 秘密の :erlang.setnode/2 :erlang.setnode/3 を使う
動いているノードの名前を変更できる **隠しコマンド** らしいです。ネットで拾いました。なにせ公式ドキュメントに何も書いてないので引数がよく分かりません。ただし以下のように関数が存在するのは確かです。ここのエラーは「そんな関数は存在しない」ではなく「引数が誤ってる」ですから。

```exs
iex(afo@10.0.1.6)6> :erlang.setnode(1,2,3) 
** (ArgumentError) argument error
    :erlang.setnode(1, 2, 3)
iex(afo@10.0.1.6)6> :erlang.setnode(0,1)  
** (ArgumentError) argument error
    :erlang.setnode(0, 1)
```

## クッキーを指定する
異なるノード間でやりとりしようとするとクッキーの指定が必要になります。これも複数の方法があります。静的に指定するのが前者ですので、今回は前者を使いました。

### rel/vm.args に記述する
先程のノード名を指定するのに使った `rel/vm.args.eex` に以下の行を付け加えてください。
```
-setcookie comecomeeverybody
```

### :erlang.set_cookie/2 を使う。
これは動いているノードにクッキーを指定するコマンドです。

```exs
iex(alice@nerves.local)1> :erlang.set_cookie(node(), :comecomeeverybody)
true
```

これは隠しコマンドでもなんでもなくて公式ドキュメント [Erlang: set_cookie/2](http://erlang.org/doc/man/erlang.html#set_cookie-2) に説明があります。

# 実行してみる
ラズパイ0Wを2つ用意して以下をやってみました。

* Nerves をインストールする
* それぞれのノード名を変更する
* それぞれのクッキーを設定する

これとは別にサーバに相当する PC で iex を起動しました。

## Alice の起動

```exs:alice
$ ssh nerves.local
...snip                    # たくさんメッセージが出るので省略
iex(alice@nerves.local)1> 
```
ちょっとたくさん出てきますが `alice@nerves.local` で立ち上がります。ネットワークインタフェースを見てみましょう。

```exs:alice
iex(alice@nerves.local)1> ifconfig
lo: flags=[:up, :loopback, :running]
    inet 127.0.0.1  netmask 255.0.0.0
    inet ::1  netmask ffff:ffff:ffff:ffff:ffff:ffff:ffff:ffff
    hwaddr 00:00:00:00:00:00

wlan0: flags=[:up, :broadcast, :running, :multicast]
    inet 10.0.1.11  netmask 255.255.255.0  broadcast 10.0.1.255
    inet fe80::ba27:ebff:fe1d:bd23  netmask ffff:ffff:ffff:ffff::
    hwaddr b8:27:eb:1d:bd:23

usb0: flags=[:up, :broadcast, :running, :multicast]
    inet 172.31.140.149  netmask 255.255.255.252  broadcast 172.31.140.151
    inet fe80::e096:3ff:fe90:eb43  netmask ffff:ffff:ffff:ffff::
    hwaddr e2:96:03:90:eb:43
```
このように、ループバック、WiFi、USB、のネットワークインタフェースが活きているのが分かります。この `usb0` はホストPCとの接続用に、`wlan0` をサーバとの接続用にします。

## Bob の起動

```exs:bob
$ ssh nerves.local
...snip                    # たくさんメッセージが出るので省略
iex(bob@nerves.local)1> 
```

同様に `bob@nerves.local` も立ち上がります。`ifconfig` すると alice 同様にネットワークインタフェースが3つできてるのが分かります。

## Server の準備
サーバは MacOS 上の iex です。固定のIPアドレスがありますので、それをノード名に含めて `server@10.0.1.6` としてクッキーと一緒に立ち上げます。

```sh
$ iex --name server@10.0.1.6 --cookie comecomeeverybody
Erlang/OTP 22 [erts-10.5.3] [source] [64-bit] [smp:4:4] [ds:4:4:10] [async-threads:1] [hipe] [dtrace]

Interactive Elixir (1.9.2) - press Ctrl+C to exit (type h() ENTER for help)
iex(server@10.0.1.6)1> 
```

念のため接続しているノードを確認しておきます。

```exs
iex(server@10.0.1.6)1> Node.list
[]
```

## Alice と BoB とをサーバに接続する
ではNervesマシンをサーバに接続してみます。

```exs:alice
iex(alice@nerves.local)2> Node.list                      
[]
iex(alice@nerves.local)3> Node.connect(:"server@10.0.1.6")
true
iex(alice@nerves.local)4> Node.list                       
[:"server@10.0.1.6"]
```

とうまいこと接続できました。Bob もやってみましょう。

```exs:bob
iex(bob@nerves.local)2> Node.list                      
[]
iex(bob@nerves.local)3> Node.connect(:"server@10.0.1.6")
true
iex(bob@nerves.local)4> Node.list                       
[:"server@10.0.1.6"]
```

と接続できます。サーバを見てみましょう。

```exs
iex(server@10.0.1.6)2> Node.list
[:"alice@nerves.local", :"bob@nerves.local"]
```
と両方の Nerves マシンとの接続が確立しているのが分かります。こうなれば Elixir の並行プログラミングはこれまで同様にできそうです。

### 全部のノードが完全グラフでつながるのではないことに注意
上で「同様にできそうです」と書いていますが、あくまでサーバとNervesノードとでの通信においてです。

同一マシン上で3つ以上のノードに `Node.connect/1` した場合は、明示的に接続を指定してないノードであっても全てのノードで全てのノードを `Node.list/0` で見ることができました。実際、全てのノードにおける任意の2ノード間で直接通信するような並行プログラミングが可能になります。ところが今回の場合はそうはなりません。

3地点で同時に `Node.list/0` を実行した結果が以下です。

```exs:server
iex(server@10.0.1.6)3> Node.list
[:"alice@nerves.local", :"bob@nerves.local"]
```
```exs:alice
iex(alice@nerves.local)5> Node.list
[:"server@10.0.1.6"]
```
```exs:bob
iex(bob@nerves.local)5> Node.list
[:"server@10.0.1.6"]
```
このように、alice と bob は直接通信ができないのです。今回 IP で届くところと言いつつ、実は alice と bob と server とは全く同一の LAN セグメント 10.0.1.0/24 上にあります。ですのでルータの経路制御とかは関係なく alice と bob を直接通信させるこが可能な環境です。これはそもそも Elixir/Erlang のノード接続の方式だとは思うのですが、もう少し時間をかけて確認したいところです。

# まとめ
広域分散環境にシステムを構築することを念頭に、Nerves の複数のマシン間での通信をさせてみました。このためにノード名の変更とクッキーの追加を行う方法を探しました。実際に実験してみて、サーバとの接続はできましたが、Nerves マシン間の直接の接続はできませんでした。

ちなみに、今回使ったような環境はたまたまそうだったと言うより、よくある話なのかと思います。一つのマシンに対して保守・管理のIFと運用のIFとがあるというのは比較的自然だからです。このような場合にでもちゃんとノードの接続が出来ないといけません。今回、ラズパイ0WのNervesを対象にしたので時間を喰いましたが、割と使い道があるのかもという気がしてきてます。

さて、明日の[#NervesJP Advent Calendar 2019](https://qiita.com/advent-calendar/2019/elixir) の記事は @gokkozemisei さんの
[エリジョになってElixirでお天気情報を取得してみた](https://qiita.com/gokkozemisei/items/a9ec29804860c5697b3b) です。こちらもお楽しみに！

# 参考文献
- [Nerves Project](https://hexdocs.pm/nerves/)
- [Elixir](https://elixir-lang.org/)
- [Erlang](https://www.erlang.org/)