ElixirでIoT#4.1.3：[仕組み篇] Docker(とVS Code)だけ！でNerves開発環境を整備する

この記事は[ #NervesJP Advent Calendar 2020 ](https://qiita.com/advent-calendar/2020/nervesjp)の6日目，の代打です．歳末助け合い運動〜:wave:
5日目は，@MzRyuKaさんの[「nerves_system_*から読み解くNervesの動向」](https://qiita.com/MzRyuKa/items/e326ee256451f2fc0f27)でした．
やっぱみんなNerves RepositoriesをWatchしてるんですね〜:eyes:

---

さて[ fukuoka.ex Elixir／Phoenix Advent Calendar 2020 ](https://qiita.com/advent-calendar/2020/fukuokaex)の4日目に，こんな記事をお送りしました．Nervesの開発環境をお手軽に整えられまっせ！！というやつです．

- [ElixirでIoT#4.1.2：[使い方篇] Docker(とVS Code)だけ！でNerves開発環境を整備する](https://qiita.com/takasehideki/items/27005ba9c0d9eb693ea9)

使い方の解説は嬉しいんだけど，どうやってるのか知りたい！！という声がいっぱい寄せられませんでした．なので本記事では，Dockerfileの記述やVS Code dev-containerの設定方法などを解説します．加えて，[某ハンズオン](https://algyan.connpass.com/event/197306/)向けに仕込んだカスタマイズ方法も紹介したいと思います．

なお本記事では，それぞれのリポジトリのRelease v0.2を対象として扱います．

- Dockerfile: https://github.com/NervesJP/docker-nerves/tree/v0.2
- VS Code: https://github.com/NervesJP/nerves-devcontainer/tree/v0.2


いろいろ使い勝手の向上のために鋭意進化中です！
もちろんcontribution大歓迎です．どしどしIssue/PRをお寄せください．
ちなみに中のヒトは docker-compose がまるでわかっていません^^;

# Dockerfile

ナマモノはこちらから．
https://github.com/NervesJP/docker-nerves/blob/v0.2/Dockerfile

いろいろコダワルとキリがないのがDocker道ですが，誰にとっても嬉しいなるべく最小限かつ共通の記述のみに留めています．DockerとかNervesに詳しい方は，自分でどんどんオレオレカスタマイズして使っていただければです．

各行それぞれを追いながら説明していきます．

## Elixir/Erlangのインストール

```dockerfile:Dockerfile
# docker-elixir 1.11.2
# https://hub.docker.com/_/elixir
FROM elixir:1.11.2
```

コメントにあるとおり，OfficialのDocker Imageを持ってきているだけです．
`Elixir 1.11.2-otp-23`のイメージを指定しています．ちなみにOSはDebian 10です．
NervesはElixir/Erlangのバージョンをちゃんと気にする必要があるので，ここはきちんとタグを指定することにしています．

なお余談ですが，Elixir 1.11.0のbugがNerves Projectで見つかって，[Joseも交えてワイワイfixされてたり](https://github.com/nerves-project/nerves/issues/557)します．なのでNervesは1.11.0と1.11.1では動作しません．

## Nerves用のパッケージのインストール

```dockerfile:Dockerfile
ENV DEBCONF_NOWARNINGS yes

# Install libraries for Nerves development
RUN apt-get update && \
    apt-get install -y build-essential automake autoconf git squashfs-tools ssh-askpass pkg-config curl && \
    rm -rf /var/lib/apt/lists/*

# [Optional] Uncomment this section to install libraries for customizing Nerves System
#RUN apt-get update && \
#    apt-get install -y libssl-dev libncurses5-dev bc m4 unzip cmake python && \
#    rm -rf /var/lib/apt/lists/*
```

Nerves公式Hexdocsの[ Installation / For Debian based systems](https://hexdocs.pm/nerves/installation.html#for-debian-based-systems )に従った`apt install`をしています．

`[Optional]`のところは，Buildrootを使ったNerves Systemのカスタマイズをしたい場合には必要です．要は上級者向けです．Docker環境で使いたい方は少ないんじゃないかなぁと思われますが，でもまぁいちおうコメントで書いておきました．

## fwupのインストール

```dockerfile:Dockerfile
# Install fwup (https://github.com/fhunleth/fwup)
ENV FWUP_VERSION="1.8.2"
RUN wget https://github.com/fhunleth/fwup/releases/download/v${FWUP_VERSION}/fwup_${FWUP_VERSION}_amd64.deb && \
    apt-get install -y ./fwup_${FWUP_VERSION}_amd64.deb && \
    rm ./fwup_${FWUP_VERSION}_amd64.deb && \
    rm -rf /var/lib/apt/lists/*
```

`fwup` とは，Nervesのファームウェアを構築したり，microSDカードへの書込みを行うためのユーティリティです．
https://github.com/fhunleth/fwup

ここでは，Debian/Ubuntu AMD64向けのapt packageをダウンロードしてインストールしています．

## Hexパッケージのインストール

```dockerfile:Dockerfile
# Install hex and rebar
RUN mix local.hex --force
RUN mix local.rebar --force
# Install Mix environment for Nerves
RUN mix archive.install hex nerves_bootstrap 1.10.0 --force
```

ビルドツールの `hex`, `rebar/rebar3` と，NervesのMix環境である `nerves_bootstrap` をインストールしています．
`mix` コマンドでのインストールはデフォルトだとインタラクティブに `Yes` する必要があり，docker buildには具合が悪いです．そのため `--force` オプションを付けています．

`nerves_bootstrap` については，ここもバージョンを揃えておいたほうが良いかなということで，明示的に指定しています．

## CMDの指定

```dockerfile:Dockerfile
CMD ["/bin/bash"]
```

最後に，コンテナ実行時のコマンドを指定しています．
実は[Elixir Official Imageでは `iex` が記述されて](https://github.com/erlef/docker-elixir/blob/f226e68615d6955ee311b8d613c3c8ca843e9f51/1.11/Dockerfile#L18)います．Nerves開発にとっては気持ちが悪いので，bashに上書きしておくことにしました．

## Tips: Docker Hubとの連携

GitHubとDocker Hubを連携したら良い感じにCI/CDできるらしいっす．

- [GitHub で自動構築 — Docker-docs-ja 17.06 ドキュメント](https://docs.docker.jp/docker-hub/github.html)

こんな感じにAutomated Builds Ruleを仕込んでます．確かに便利〜
（`algyan` は[某ハンズオン用](https://algyan.connpass.com/event/197306/)です）

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/240349/141ea370-3dcf-5eb2-86eb-5a0a6ce57c44.png)


# VS Code設定

次はVS Code dev-containerの設定です．コレ自体の解説は，他の記事を読んだほうが早いです．

- (MS公式) [Visual Studio Code Remote Development](https://code.visualstudio.com/docs/remote/remote-overview)
- [VSCode Remote Containerが良い - Qiita](https://qiita.com/d0ne1s/items/d2649801c6f804019db7)
- [DockerとRemote Containersでの開発環境が最高過ぎる - Sweet Escape ](https://www.keisuke69.net/entry/2020/06/04/145719)

設定方法は簡単で， `./devcontainer/devcontainer.json` というファイルでポチポチとプロパティを書いていくだけです．Properties referenceはこんな感じ．
https://code.visualstudio.com/docs/remote/devcontainerjson-reference

ということでナマモノはこちらから．
https://github.com/NervesJP/nerves-devcontainer/blob/v0.2/.devcontainer/devcontainer.json

コチラも各行それぞれを追いながら説明していきます．

## コンテナ名とイメージの指定

```json:./devcontainer/devcontainer.json
{
	"name": "Nerves",
	// Pull Docker image from DockerHub  https://hub.docker.com/r/nervesjp/nerves
	"image": "nervesjp/nerves:0.2",
	// NOTE: original Dockerfile is maintained at https://github.com/NervesJP/docker-nerves/blob/main/Dockerfile
	// You can also build Docker image locally by locating above file to here and uncomment next line 
	//"dockerFile": "Dockerfile",
```

コンテナ名を"Nerves"にしています．これがVS Code左下ステータスバーの緑色のところに表示されます．
イメージは，Docker Hubにpushしてあるビルド済みのイメージを指定しています．コメントにもある通り，Dockerfileをローカルに置いて直接ビルドすることもできます．

## マウントの指定

```json:./devcontainer/devcontainer.json
	"mounts": [
		"source=${localWorkspaceFolder}/.hex,target=/root/.hex,type=bind,consistency=cached",
		"source=${localWorkspaceFolder}/.nerves,target=/root/.nerves,type=bind,consistency=cached",
		"source=${localWorkspaceFolder}/.ssh,target=/root/.ssh,type=bind,consistency=cached"
	],
```

ホストとDockerコンテナの間でマウントするディレクトリを指定しています．
ここ割りとコダワリどころで，Nerves開発に使う上記３つのディレクトリをマウントするようにしました．これらはカレントディレクトリ（GitHubのディレクトリ）にもともと作ってあります．`${HOME}`でも良いかなと悩んだのですが，Nervesはじめてな方はこれらのディレクトリが無いかなということで，この形式に落ち着いています．

なお dev-container を開いたホストのディレクトリは，コンテナ内の `/workspaces/nerves-devcontainer` に自動的にマウントされます．

## シェルの設定

```json:./devcontainer/devcontainer.json
	"settings": {
		"terminal.integrated.shell.linux": "bash"
	},	
```

まぁ定番ということでbashを指定しています．
ここは各人でコダワリあるかもしれませんが，宗教戦争は控えましょう^^;

## VS Code拡張機能のインストール

```json:./devcontainer/devcontainer.json
	"extensions": [
		"jakebecker.elixir-ls"
	],
```

コンテナ内のVS Codeにて使用する拡張機能を指定しています．
とりあえずベタに[ElixirLS](https://marketplace.visualstudio.com/items?itemName=JakeBecker.elixir-ls)だけにしてあります．オススメのプラグイン教えてください〜

## Nerves向け環境変数の設定

```json:./devcontainer/devcontainer.json
	// This section sets environment variables for Nerves development
	"remoteEnv": {
		"MIX_TARGET": "rpi3",
		"WIFI_SSID": "xxxxxxxx",
		"WIFI_PSK": "yyyyyyyy"
	}
```

[[使い方篇 / 環境変数の設定] ](https://qiita.com/takasehideki/items/27005ba9c0d9eb693ea9#%E7%92%B0%E5%A2%83%E5%A4%89%E6%95%B0%E3%81%AE%E8%A8%AD%E5%AE%9A-1)でも紹介していますが，VS Code拡張機能の便利なところで，コンテナ内のシェル環境変数を設定することができます．

よく忘れがちな `${MIX_TARGET}` を入れておきました．
あとはVintageNet WiFiの設定をするための環境変数です．これらの環境変数の使い方は @kentaro さんのナイスな記事をご参照いただくのが良いかと．

- [ウェブチカでElixir/Nervesに入門する（2020年12月版） / Wi-Fi経由でファームウェアをデプロイできるようにする](https://qiita.com/kentaro/items/e8df79aa93b9fe9a567e#wi-fi%E7%B5%8C%E7%94%B1%E3%81%A7%E3%83%95%E3%82%A1%E3%83%BC%E3%83%A0%E3%82%A6%E3%82%A7%E3%82%A2%E3%82%92%E3%83%87%E3%83%97%E3%83%AD%E3%82%A4%E3%81%A7%E3%81%8D%E3%82%8B%E3%82%88%E3%81%86%E3%81%AB%E3%81%99%E3%82%8B)

ご利用の際には，これらをお好みに合わせていただければです．

## ユーザの設定

```json:./devcontainer/devcontainer.json
	// Uncomment to connect as a non-root user. More info: https://aka.ms/vscode-remote/containers/non-root.
	//"remoteUser": "vscode"
}
```

コンテナ内のユーザを設定できますが，まぁrootでも良いかなということでコメントにしてあります．

# ハンズオンに向けた調整

[[使い方篇] ](https://qiita.com/takasehideki/items/27005ba9c0d9eb693ea9#%E3%81%AA%E3%81%AB%E3%81%8C%E5%AC%89%E3%81%97%E3%81%84%E3%81%AE%E3%81%84%E3%81%A4%E4%BD%BF%E3%81%86%E3%81%AE)では，devcontainerこんな嬉しさがあるぜっ！ってのを紹介しました．

- ハンズオンの開催を計画する時にすごく頭を悩ませるのが，参加者の環境を揃えることです．DockerfileとかVS Codeの各種設定ファイルを調整すれば，これもお手軽に解消できます！！
    - |> [［PR］参考例［広告］](https://algyan.connpass.com/event/197306/)
    - |> [この例の導入方法［ステマ］](https://qiita.com/takasehideki/items/79d4ba3f95b1463105f8)

もともとdev-container環境を整備したのは，このハンズオンに向けた準備のためでした．

- ALGYAN主催！[【オンライン】豪華プレゼント付！Elixir/Nerves(ナーブス)体験ハンズオン！
](https://algyan.connpass.com/event/197306/)

じゃあどうやってカスタマイズしているの？を紹介します．

## Dockerfile

Dockerfile側は，v0.1から `algyan` branchを生やして整備しました．
GitHubのWebページではこんな感じです．
https://github.com/NervesJP/docker-nerves/compare/v0.1...algyan

ターミナルではこんな感じです．これもブロックごとに説明しましょう．

```diff
$ git diff v0.1..algyan Dockerfile 
diff --git a/Dockerfile b/Dockerfile
index 95ce84e..5cbadf5 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -29,4 +29,28 @@ RUN mix local.rebar --force
 # Install Mix environment for Nerves
 RUN mix archive.install hex nerves_bootstrap --force
 
+# Download archives of Nerves artifacts on Docker build process
+RUN mkdir -p ~/.nerves/dl
+# Alghough the latest version of nerves_system_rpi4 is now v1.13.1, we found
+# the issue about WiFi connection to 5GHz AP on this version.
+# So we decided to download both versions to avoid confusion on ALGYAN hands-on.
+RUN wget -q -P ~/.nerves/dl/ https://github.com/nerves-project/nerves_system_rpi4/releases/download/v1.13.0/nerves_system_rpi4-portable-1.13.0-366303C.tar.gz
+RUN wget -q -P ~/.nerves/dl/ https://github.com/nerves-project/toolchains/releases/download/v1.3.2/nerves_toolchain_arm_unknown_linux_gnueabihf-linux_x86_64-1.3.2-E31F29C.tar.xz
+RUN wget -q -P ~/.nerves/dl/ https://github.com/nerves-project/nerves_system_rpi4/releases/download/v1.13.1/nerves_system_rpi4-portable-1.13.1-C916C86.tar.gz
+RUN wget -q -P ~/.nerves/dl/ https://github.com/nerves-project/toolchains/releases/download/v1.3.2/nerves_toolchain_aarch64_unknown_linux_gnu-linux_x86_64-1.3.2-7C57FE3.tar.xz
+
```

今回の準備で特に気にしたことは，ハンズオンの最中の通信量を少しでも削減することでした．
ALGYANイベントはMS Teamsでオンライン開催ですので，負荷は小さいほうがよいです．また，ラズパイ4を参加者にプレゼント！ということで，ターゲットは`rpi4`だけ気にすればよいです．

ということで，`mix deps.get`の際にダウンロードしてくるnerves_systemパッケージをコンテナ内に入れ込むことにしました．
v1.13.0とv1.13.1の両方があるのは，まぁいろいろな事情があります^^;

```diff
+# COPY SSH keys (id_rsa / id_rsa.pub) to `/root/.ssh/` on Docker dev-container
+# Of course we understand that sharing SSH keys on GitHubit is dangerous, but
+# we employ this way to make the hands-on process more efficient.
+RUN mkdir -p /root/.ssh
+COPY .ssh/id_rsa /root/.ssh/
+RUN chmod 600 /root/.ssh/id_rsa
+COPY .ssh/id_rsa.pub /root/.ssh/
+COPY .ssh/config /root/.ssh/
+RUN chmod 600 /root/.ssh/config
+
```

今回のハンズオンでは，Nervesファームウェアをもう書き込み済み！のmicroSDカードをプレゼントしています．これは，[dev-containerからは`mix burn`できない,,,](https://qiita.com/takasehideki/items/27005ba9c0d9eb693ea9#fwup%E3%81%AE%E3%82%A4%E3%83%B3%E3%82%B9%E3%83%88%E3%83%BC%E3%83%AB-aka-%E5%88%B6%E7%B4%84%E4%BA%8B%E9%A0%85)という制約から来ています．
その代わり，SSH鍵をコンテナに入れ込むことができるので，そのあたりの設定をスキップすることができます．ただし，SSH鍵をみんなで同じの使い回すという，ちと危険が危ないやつですが^^;

ちなみに `.ssh/config` はこんな設定を書いてあります．

```.ssh/config
Host nerves.local
    ServerAliveInterval 10
    StrictHostKeyChecking no
```

最後に，シェルの設定をちょっと入れ込んでいます．

```diff
+# Append some environmental variables for hands-on
+RUN echo "export PS1=\"\n\[\033[0;32m\]\u@\h \[\033[0;33m\]\w\n\\[\033[0m\]# \[\033[0m\]\"" >> ~/.bashrc
+RUN echo "export MIX_TARGET=rpi4" >> ~/.bashrc
+
 CMD ["/bin/bash"]
```

これも`rpi4`だけ気にすればよいので，`${MIX_TARGET}`を設定してあります．
また，`${PS1}`は私の好みです^^;

なおVS Code拡張機能の`remoteEnv`オプションを知ったのは，このあとのお話しでしたｗ

## VS Code設定

こちらはハンズオンの混乱を避けるために，v0.1からファイルをコピってリポジトリをまるごと分けました．
https://github.com/NervesJP/nerves-algyan-devcontainer

<a href="https://github.com/NervesJP/nerves-algyan-devcontainer"><img src="https://github-link-card.s3.ap-northeast-1.amazonaws.com/NervesJP/nerves-algyan-devcontainer.png" width="460px"></a>

またもや `git diff` を追って見ていきます．

```diff
$ git diff c17534 .devcontainer/devcontainer.json 
diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json
index 5ef137c..924e3f7 100644
--- a/.devcontainer/devcontainer.json
+++ b/.devcontainer/devcontainer.json
@@ -1,10 +1,17 @@
 {
-       "name": "Nerves",
-       "dockerFile": "Dockerfile",
-       "extensions": [
-               "jakebecker.elixir-ls"
-       ],
+       "name": "NervesJP-ALGYAN",
+       "image": "nervesjp/nerves:algyan",
+
+       //"mounts": [
+       //      "source=${localWorkspaceFolder}/.ssh,target=/root/.ssh,type=bind,consistency=cached"
+       //],
+
        "settings": {
                "terminal.integrated.shell.linux": "bash"
-       }
+       },
```

こっちはシンプルですね．
コンテナの名前を "NervesJP-ALGYAN" に変更しているのと，pullしてくるDocker imageのタグを `:algyan` にしているだけです．タグの変更だけでDocker Hub連携をよしなにできるの，マジ便利です．
あと `.ssh/` 等のバインドはやめています．

# おわりに

こんな感じで，おれのかんがえた最強のNerves dev-container環境を整えてみてください！！

---
[ #NervesJP Advent Calendar 2020 ](https://qiita.com/advent-calendar/2020/nervesjp)の7日目は @myasu さんによる[「Elixir/NervesでPLC(多機能リレー)を作ってみた」](https://qiita.com/myasu/items/a6664d3a3c76503affc7)です．PLCまるでわからん:thinking: