ラズパイ上の Docker コンテナに VS Code の Remote Containers で接続する

# はじめに
Docker を使い始めてからというもの、開発環境を簡単に作り直すことができるようになったので、うっかり環境を壊してしまう恐怖から開放されて気軽にあれこれ試せるようになりました。ただし、Docker コンテナ内で動かす用のコードを VS Code で編集すると、環境が異なるので構文エラーになったり補完が効かなかったりして、不便に感じることもあると思います。

それを解消してくれる VS Code の拡張機能が Remote Containers です。Docker コンテナに接続して、コンテナ内の環境でコードを編集できるようになります。

やり方は [VS Code のドキュメント](https://code.visualstudio.com/docs/remote/containers-advanced#_developing-inside-a-container-on-a-remote-docker-host)にありますので、それを参考に進めます。

# リモートのセットアップ
Remote Containers はローカルだけでなくリモートにある Docker コンテナに接続することもできます。ここでは、<b>Raspberry Pi 3B+</b> 上で Docker コンテナを立ち上げて、ローカルの VS Code からコンテナ内に入れるようにしてみます。

## Raspberry Pi の設定
### 用意するもの
- Raspberry Pi OS (32bit) セットアップ済みの Raspberry Pi 3B+
- Ubuntu インストール済みの PC

### SSHの有効化
Raspberry Pi の [SSH を有効化](https://www.raspberrypi.org/documentation/remote-access/ssh/)します。スタートメニューから、設定(Preference) -> Raspberry Pi の設定 (Raspberry Pi Configuration) の、インターフェース (Interfaces) タブから設定できます。

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/248231/b63bd969-fb7c-583e-2336-8d9b4d3c1141.jpeg" width="640"/>

### 固定IPアドレスの設定
Raspberry Pi の[固定IPアドレスを設定](https://www.raspberrypi.org/documentation/configuration/tcpip/)します。```/etc/dhcpcd.conf``` を編集して、ネットワーク内の他の機器に被らないアドレスを指定します。ここでは 192.168.0.51 にしました。

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/248231/146ee6ed-d4a2-c17f-4458-ed2d2980b4dd.jpeg" width="640"/>

### Docker のセットアップ
Raspberry Pi に Docker をインストールします。インストール用のスクリプトがあるので次のコマンドを実行するだけです。（[公式のドキュメント](https://docs.docker.com/engine/install/debian/#install-using-the-convenience-script)には本番環境での使用は推奨されないと書いてありますが、今回は実験なので目をつぶります。）

```sh
$ curl -fsSL https://get.docker.com -o get-docker.sh
$ sudo sh get-docker.sh
```
root 以外のユーザーから docker コマンドを起動できるように設定します。

```sh
$ sudo usermod -aG docker $USER
$ newgrp docker
```

Dockerが正常に動作しているか確認するため、 [ubuntu:18.04](https://hub.docker.com/layers/ubuntu/library/ubuntu/18.04/images/sha256-eccb49f09e6877d11da55b52e262114b0c87cfecbf578f683a20bf903fccecea?context=explore) のコンテナを走らせてみます。

```sh
$ docker run -it --rm ubuntu:18.04
```
次のような出力が得られたら大丈夫です。Ctrl+D で終了させます。

```sh
Unable to find image 'ubuntu:18.04' locally
18.04: Pulling from library/ubuntu
9ac910c0311f: Pull complete 
5b89a6b5bccf: Pull complete 
36058ccabae5: Pull complete 
Digest: sha256:4bc3ae6596938cb0d9e5ac51a1152ec9dcac2a1c50829c74abd9c4361e321b26
Status: Downloaded newer image for ubuntu:18.04
root@c956f3eea22c:/#
```

# VS Code の設定
PC に戻って VS Code の設定を行います。

## 拡張機能のインストール
VS Code の拡張機能を開いて、 Docker と Remote Containers がインストールされていることを確認します。インストールされていなければ、ここでインストールします。

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/248231/ec015370-c3a5-a099-9a32-eafa5124a84f.png" width="360"/>  <img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/248231/534899af-5fa5-4c1a-6d9f-8e948e6d7c9c.png" width="360"/>


## リモート Docker ホストの設定
File -> Preferences -> Settings から設定画面を開きます。検索欄に ```docker.host``` と入力すると、Docker ホストを設定する項目が出てくるので、```ssh://myuser@mymachine``` のかたちでホストを入力します。

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/248231/410d8617-1cc7-edce-99d7-8a442c6845a3.png" width="640"/>

## SSH 鍵の設定
これ以降、SSH 鍵を設定していないと何度もパスワードの入力を求められます。面倒なので、[Quick start: Using SSH keys](https://code.visualstudio.com/docs/remote/troubleshooting#_quick-start-using-ssh-keys) を参考に、次のようなコマンドで SSH 鍵を作成して設定します。

```sh
$ ssh-keygen -t rsa -b 4096
$ ssh-copy-id -i ~/.ssh/id_rsa.pub pi@192.168.0.51
```

## 接続テスト
[devcontainer.json](https://code.visualstudio.com/docs/remote/containers#_create-a-devcontainerjson-file) ファイルを作成します。作業ディレクトリで ```.devcontainer/devcontainer.json``` ファイルを作って下の例のように入力します。

Remote Containers は、デフォルトで作業ディレクトリをコンテナにマウントするようになっていますが、ローカルのファイルをそのままリモートにマウントすることはできないらしく、この例では作業ディレクトリの代わりに [Named volume](https://docs.docker.com/storage/volumes/) をマウントしています。

```json
{
    "image": "ubuntu",
    "workspaceFolder": "/workspace",
    "workspaceMount": "source=remote-workspace,target=/workspace,type=volume"
}
```

準備が出来たら Remote Containers を走らせます。F1 キーか、画面右下の緑ボタンをクリックして、```Remote-Containers: Reopen in Container``` を選択します。

しかしエラーが発生しました。どうやらこのコンテナには libatomic というライブラリが無いようです。

```sh
Remote-Containers server: /root/.vscode-server/bin/622cb03f7e070a9670c94bae1a45d78d7181fbd4/node: error while loading shared libraries: libatomic.so.1: cannot open shared object file: No such file or directory
```
というわけで、Raspberry Pi 上で Dockerfile を作って、libatomic をインストールした Docker イメージをビルドします。

```Dockerfile
FROM ubuntu:18.04

RUN apt-get update && apt-get install -y libatomic1
```
```sh
$ docker build -t ubuntu .
```

VS Code に戻って今度は ```Remote-Containers: Rebuild and Reopen in Container``` を実行すると接続することができました。

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/248231/3171be13-6c28-2367-821e-a674423b193a.png" width="640"/>

# その他の設定
上の操作で接続することはできましたが、これだけだとあまり便利ではないので以下の設定を加えます。

## リモートのファイルのマウント
ローカルのファイルは Named volume 経由でないとマウントできないようですが、リモートのファイルは直接マウントすることができます。例えばリモートに ```/home/pi/workspace``` というディレクトリがあって、Docker コンテナの ```/workspace``` にマウントしたい場合は、```devcontainer.json``` の ```"workspaceMount"``` を次のように変更することでマウントできます。

```json
"workspaceMount": "source=/home/pi/workspace,target=/workspace,type=bind,consistency=cached",
```

## 非 root ユーザーの追加
上で使用したコンテナのユーザーは root でした。Remote Containers は root で利用することもできますが、Permission 関連の問題が生じることが多いので、ユーザーを追加しておいたほうが無難です。[ドキュメント](https://code.visualstudio.com/docs/remote/containers-advanced#_creating-a-nonroot-user)を参考に、Dockerfile に次の行を追加します。ユーザー名は割と何でもいいのですが、この例では vscode です。

```Dockerfile
ARG USERNAME=vscode
ARG USER_UID=1000
ARG USER_GID=$USER_UID

# Create the user
RUN groupadd --gid $USER_GID $USERNAME \
    && useradd --uid $USER_UID --gid $USER_GID -m $USERNAME \
    && apt-get update \
    && apt-get install -y sudo \
    && echo $USERNAME ALL=\(root\) NOPASSWD:ALL > /etc/sudoers.d/$USERNAME \
    && chmod 0440 /etc/sudoers.d/$USERNAME

# Set the default user
USER $USERNAME
```

## 拡張機能を再インストールしないよう設定
Remote Containers を使うと、VS Code の拡張機能は Docker コンテナ内の ```~/.vscode-server/extensions``` と ```~/.vscode-server-insiders/extensions``` にインストールされます。そのためコンテナを再ビルドすると拡張機能もインストールし直す必要が生じてしまいますが、以下の手順でそれを回避できます。

- Docker イメージに```~/.vscode-server/extensions``` と ```~/.vscode-server-insiders/extensions``` を作成します。[ドキュメント](https://code.visualstudio.com/docs/remote/containers-advanced#_avoiding-extension-reinstalls-on-container-rebuild)に従って Dockerfile に次の行を追加します。

    ```Dockerfile
    RUN mkdir -p /home/$USERNAME/.vscode-server/extensions \
            /home/$USERNAME/.vscode-server-insiders/extensions \
        && chown -R $USERNAME \
            /home/$USERNAME/.vscode-server \
            /home/$USERNAME/.vscode-server-insiders
    ```

- devcontainer.json を編集して、作成したディレクトリに Named volume をマウントするように設定を追加します。unique-vol-name-here には他のコンテナと被らない名前で上書きします。

    ```json
    "mounts": [
		"source=unique-vol-name-here,target=/home/vscode/.vscode-server/extensions,type=volume",
		"source=unique-vol-name-here-insiders,target=/home/vscode/.vscode-server-insiders/extensions,type=volume",
    ]
    ```

## Dockerfile と devcontainer.json
上記の設定を反映した結果、Dockerfile と devcontainer.json は次のようになりました。

```Dockerfile
FROM ubuntu:18.04

RUN apt-get update && apt-get install -y libatomic1

ARG USERNAME=vscode
ARG USER_UID=1000
ARG USER_GID=$USER_UID

# Create the user
RUN groupadd --gid $USER_GID $USERNAME \
    && useradd --uid $USER_UID --gid $USER_GID -m $USERNAME \
    && apt-get update \
    && apt-get install -y sudo \
    && echo $USERNAME ALL=\(root\) NOPASSWD:ALL > /etc/sudoers.d/$USERNAME \
    && chmod 0440 /etc/sudoers.d/$USERNAME

# Set the default user
USER $USERNAME

# Create extension directory
RUN mkdir -p /home/$USERNAME/.vscode-server/extensions \
        /home/$USERNAME/.vscode-server-insiders/extensions \
    && chown -R $USERNAME \
        /home/$USERNAME/.vscode-server \
        /home/$USERNAME/.vscode-server-insiders
```

```json
{
	"image": "ubuntu",
	"workspaceFolder": "/workspace",
	"workspaceMount": "source=/home/pi/workspace,target=/workspace,type=bind,consistency=cached",
	"mounts": [
		"source=remote-extensions,target=/home/vscode/.vscode-server/extensions,type=volume",
		"source=remote-extensions,target=/home/vscode/.vscode-server-insiders/extensions,type=volume",
	]
}
```

# おわりに
Raspberry Pi 上のコンテナに、PC の VS Code から接続することができました。Raspberry Pi だけでなく [Jetson](https://developer.nvidia.com/embedded/learn/tutorials/jetson-container) など他のシングルボードコンピュータや、遠隔地のサーバーにも利用できるそうなので、いろいろと便利な開発環境が作れそうです。

# 参考
- [Advanced Container Configuration](https://code.visualstudio.com/docs/remote/containers-advanced)
- [Raspberry Pi Documentation](https://www.raspberrypi.org/documentation/)
- [Remote Development Tips and Tricks](https://code.visualstudio.com/docs/remote/troubleshooting)
- [Raspberry Piにdockerをインストールする](https://qiita.com/HyunwookPark/items/6e4854248c6d63958060)
- [VS CodeのRemote-Containersでリモートサーバー上のコンテナーの開発を行う方法](https://qiita.com/tnk4on/items/7ccb1921907bdb99002a)
- [VSCodeでリモートホスト上のDocker上で開発する](https://qiita.com/sgmryk/items/e8178fbf246b2c7aad81)