VS Code Remote Development で Docker 開発環境を利用する

2019年5月、Visual Studio Code でリモートマシン、コンテナ、Windows Subsystem for Linux (WSL) 上のワークスペースを開くことができるようになる拡張機能群「**Remote Development Extension Pack**」が紹介されました。
Blog 記事: https://code.visualstudio.com/blogs/2019/05/02/remote-development

公式のドキュメントは [Visual Studio Code Remote Development](https://code.visualstudio.com/docs/remote/remote-overview) で、以下、そちらを参照しながら VS Code で Docker コンテナ内の開発環境を開けるようになるまでの試行錯誤の顛末です。
環境は以下の通り。

- macOS Mojave 10.14.5
- Docker Desktop for Mac Version 2.0.0.3 (31259)
    - Engine: 18.09.2
    - Compose: 1.23.2

なお、今回の実験に利用したリポジトリは https://github.com/ykhrito/remote-dev に置いてあります。

## インストール

### Visual Studio Code

Stable 版では May 2019 (version 1.35) リリースから Remote Development 拡張が利用可能になりました。バージョンが古い場合はアップデートしてください。
リリースノート: https://code.visualstudio.com/updates/v1_35

### Remote - Containers extension

Visual Studio Code を起動し、拡張機能ビューを表示します。とりあえず Docker を試すので [Remote - Containers](https://marketplace.visualstudio.com/items?itemName=ms-vscode-remote.remote-containers) 拡張をインストールします。（スクリーンショットは Japanese Language Pack を先にインストールしたので日本語インターフェイスになっています）
<img width="777" alt="Extension__Remote_-_Containers.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/16187/1a3ced3e-5f31-de69-f156-9cb80c3a20b2.png">
インストールが完了するとエディタウィンドウの左下にリモート接続のアイコンボタンが表示されます。
<img width="109" alt="Extension__Remote_-_Containers-2.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/16187/43d389d9-e1b9-e8d3-1efb-f21ecee160bb.png">

## 設定

docker-compose.yml で Ruby on Rails の開発を行うためのリポジトリがあるものと想定し、そこにリモート開発用の設定を追加していきます。ディレクトリの構成としては以下のような感じ。

```
.
├── .devcontainer
│   ├── devcontainer.json
│   └── docker-compose.extend.yml
├── .gitignore
├── .ruby-version
├── Gemfile
├── Gemfile.lock
...
├── docker-compose.yml
...
``` 

### docker-compose.yml

[circleci/ruby](https://hub.docker.com/r/circleci/ruby) および [mysql](https://hub.docker.com/_/mysql) イメージを利用しています。[circleci/ruby](https://hub.docker.com/r/circleci/ruby) はリンク先の Description にもありますがデフォルトで root ではなく circleci というユーザーで動作する点に注意。

```yaml
version: "3"
services:
  web:
    image: circleci/ruby:2.6.3-node-browsers
    working_dir: /home/circleci/remote-dev
    command: sleep infinity
    ports:
      - 3000:3000
    links:
      - mysql
    volumes:
      - ./:/home/circleci/remote-dev
    environment:
      PORT: 3000
      DATABASE_HOST: mysql
  mysql:
    image: mysql:5.7
    environment:
      MYSQL_ALLOW_EMPTY_PASSWORD: "yes"
```
リポジトリのディレクトリは circleci ユーザーのホームディレクトリ以下の remote-dev にマウントされます。

### .devcontainer/docker-compose.extend.yml

既存の docker-compose.yml の設定には手を加えず、リモート開発用に追加したい設定をこちらに記述します。

```yaml
version: "3"
services:
  web:
    environment:
      - HOME=/home/circleci
    volumes:
      - ~/.gitconfig:/home/circleci/.gitconfig
      - ~/.ssh:/home/circleci/.ssh
```
`"environment"` で環境変数 `HOME` を明示的に指定しています。この設定が無いとリモート開発サーバや拡張機能等の環境が /root/ 以下にインストールされてしまうため、一般ユーザーで動作させる場合にはアクセス権がないので失敗してしまいます。root で動くコンテナなら設定不要です。

`"volumes"` の設定はコンテナ内の環境でホストと同じ設定で git を使えるようにするためのものです。

### .devcontainer/devcontainer.json

リモート開発環境設定の本丸。

```javascript
// See https://aka.ms/vscode-remote/devcontainer.json for format details.
{
  "name": "Remote-dev Project",
  "dockerComposeFile": [
    "../docker-compose.yml",
    "docker-compose.extend.yml"
  ],
  "service": "web",
  "workspaceFolder": "/home/circleci/remote-dev",
  "extensions": [
    "rebornix.ruby",
    "castwide.solargraph",
    "robinbentley.sass-indented"
  ],
  "settings": {
    "editor.tabSize": 2,
    "files.insertFinalNewline": true,
    "files.trimFinalNewlines": true,
    "terminal.integrated.shell.linux": "/bin/bash"
  },
  "shutdownAction": "none" // or "stopCompose"
}
```

`"name"` は VS Code に表示されるワークスペース名。なんでも良いと思います。
`"dockerComposeFile"` で元々の docker-compose.yml と追加の docker-compose.extend.yml を同時に指定して実行させます。
`"service"` は開発環境として利用するサービスの定義名。
`"workspaceFolder"` は VS Code で開くワークスペースのディレクトリ。
`"extensions"` でリモート開発環境に一緒にインストールする拡張機能を指定します。リモートにインストールできない/しなくてもよい拡張もあるので、無理にここで指定せずにあとで手動でインストールしても良いと思います。
`"settings"` でリモート開発環境のデフォルトの設定（settings.json の内容）をあらかじめ追加できます。コンテナ固有の設定が必要な場合には便利だと思います。
`"shutdownAction"` は VS Code を閉じた際の挙動です。デフォルトは "stopCompose" でコンテナが自動的に停止します。"none" にするとコンテナは起動したままです。

### リモート接続

Visual Studio Code を起動し、先ほどの左下のリモート接続アイコンをクリック。
「Remote-Containers: Open Folder in Container...」を選択し、リポジトリのディレクトリを開きます。
<img width="731" alt="Untitled-1.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/16187/f28fea22-bf79-cbb0-88c3-2122d0ecf778.png">
自動的にコンテナのビルド・起動が行われ…
<img width="468" alt="payment-proto__Dev_Container__Payment-proto_Project__と_Notion_Desktop.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/16187/3837dc3a-6659-5b8a-cd0d-52434344d0ea.png">
しばらく待つと…
<img width="1115" alt="remote-dev__Dev_Container__Remote-dev_Project_.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/16187/33fac2f8-7df6-fc54-640a-20af2d685993.png">
コンテナ内の開発環境が起動し、VS Code で開かれました。

統合ターミナルやソース管理も普通に利用可能になっています。"settings" で /bin/bash を指定したので、bash が起動しています。
<img width="699" alt="remote-dev__Dev_Container__Remote-dev_Project_-2.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/16187/52be95fa-271f-47d1-620a-9bb2e2df08e6.png">

## 動作確認

試しに Rails を起動して接続できるか試してみます。ターミナルで `gem install bundler:2.0.1`、`bundle install`、`bin/rails db:create` したのち `bin/rails s -b 0.0.0.0` でサーバーを起動し
<img width="760" alt="remote-dev__Dev_Container__Remote-dev_Project_-3.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/16187/907b4aad-5ea1-aa4c-d5dd-7c9a02769d62.png">
ホストのブラウザから接続してみると
<img width="630" alt="Ruby_on_Rails.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/16187/94669364-46c5-14ec-81a4-1bb6f1a1a151.png">
Yay!

## その他補足など

### ~/.ssh/config

macOS ホスト上の ~/.ssh をコンテナ内の ~/.ssh にマウントしていますが、~/.ssh/config に `UseKeychain yes` と設定している場合、コンテナ側の git で ssh を利用する場合に「Bad configuration option: usekeychain」と怒られてしまいます。その場合は 'UseKeychain yes' の前に `IgnoreUnknown UseKeychain` と記述しておくとよいようです。

```text:~/.ssh/config
Host *
  AddKeysToAgent yes
  IgnoreUnknown UseKeychain
  UseKeychain yes
```