VSCode と devcontainer で始める開発環境構築

この記事は [WESEEK Advent Calendar 2020](https://qiita.com/advent-calendar/2020/weseek) 13日目の記事です。
# はじめに
みなさん docker は普段使っていますか？
docker いいですよね。Dockerfile や docker-compose.yml などのコードさえ用意して docker の実行環境さえ用意しておけば誰も簡単に同じ環境の構築ができます。
そんな docker の特性を活かし、めんどくさい開発環境の構築を一発で完了させ、しかも誰もが同じ環境で開発できる方法を紹介したいと思います。

# VSCode の devcontainer とは？
VSCode の extension の Remote - Containers を使って立ちあげる開発環境のことを指しています。  
公式ドキュメントは [こちら](https://code.visualstudio.com/docs/remote/containers)  
この仕組みを利用することで VSCode と docker の動く環境さえあればだれでもボタン数発で開発環境を構築することができ、すぐにコードを書き始めることができます。

# 利用方法

## docker/docker-compose インストール
本題ではないので [公式](https://www.docker.com/get-started) を見て各々の環境にインストールしましょう。

Windows の場合は [WSL2](https://docs.microsoft.com/ja-jp/windows/wsl/install-win10) と [Docker Desktop for Windows](https://www.docker.com/get-started) の組み合わせ、
Mac の場合は [Docker Desktop for Mac](https://www.docker.com/get-started) を使うのが一般的かと思います。
Docker さえ動くようにしてしまえば OS の差異は Docker が吸収してくれるので同じ環境が構築できると言えるわけです。

## VSCode 及び extension インストール
VSCode は [公式ページ](https://code.visualstudio.com/) よりダウンロードしてインストール。  
必要な VSCode の extension は VSCode 左上の ブロックみたいなマークをクリックし、出てきた検索窓に `remote` と打ち込むと画像のような extension 達が出てくると思います。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/100251/b672a2cb-ac71-70f0-f6f3-61dbd9fb8b10.png)
自分の VSCode にはすでにインストール済みなので左上に星マークがついていますが未インストールの場合は付いてないかと思います。
`Remote - Containers` と Windows で WSL を使う場合は `Remote - WSL` の両方をインストールしましょう。

## 起動方法
**※上記必要なプロダクトのインストールは終わっている前提です。**
前に自分が試しに触ってみた [Blitz.js のプロジェクト](https://github.com/haruhikonyan/blitz-first)の立ち上げを例にしてみたいと思います。

### devcontainer 利用の確認
リポジトリの中身を見て行くと https://github.com/haruhikonyan/blitz-first/tree/master/.devcontainer こんなフォルダがあります。
リポジトリ内に [.devcontainer](https://github.com/haruhikonyan/blitz-first/tree/master/.devcontainer) フォルダとその中に [devcontainer.json](https://github.com/haruhikonyan/blitz-first/blob/master/.devcontainer/devcontainer.json) が入っていればそのプロジェクトでは VSCode と devcontainer を使って統一された開発環境を苦無く立ち上げて開発しているんだなと思ってよいと思います。

### devcontainer 起動
Windows では WSL の中、Mac では適当なディレクトにリポジトリを clone します。clone したら VSCode を起動し、画面一番左下の `><` っぽいところをクリックします。するとスクショのようなメニューが出てくるかと思います。  
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/100251/ae27f1e9-56fc-1075-286a-ff64cf48513f.png)
すでに VScode でリポジトリを開いている場合は `Remote-Containers: Reopen in Container` を選択します。
リポジトリを開いてない場合は `Remote-Containers: Open Folder in Container` を選択し、リポジトリ root を選択しましょう。
すると VSCode が devcontainer.json の設定を読み込み、必要なコンテナの立ち上げなどが行われます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/100251/8ed91daa-2fcc-fc42-de2c-d4865124351f.png)
実際にどんな処理が行われているかどうか気になる人は `show log` をクリックするとコンテナの立ち上がる様子が確認できます。
しばらく待ち、必要なコンテナがすべて立ち上がるとコンソールが立ち上がります。(`show log` を見た方は VSCode 下部の TARMINAL タブ右側 `+` ボタンをクリックして新しくコンソールを開きましょう。)
あとはもう普段と同じように node であれば `yarn` や `npm` などを利用して開発環境を立ち上げるコマンドを実行すれば開発環境が起動します。
例の blitz.js では `blitz start` を実行すると開発用のサーバが立ち上がり、`localhost:3080` にアクセスすると画面が表示されると思います。

# 開発環境構築方法
**※ あくまで自分が新しく開発環境を構築するために取る手法であって必ずこの手順を踏まなければいけないというわけではありません。**

利用方法で例として出した [blitz.js](https://blitzjs.com/) の開発環境構築をする方法を紹介します。
### 1. 立ち上げるコンテナを何にするかを決める
今回はフロントエンドからバックエンド、そして今話題の ORM である  [prisma](https://www.prisma.io/) を搭載したフルスタックフレームワークである [blitz.js](https://blitzjs.com/) を動かすので node だけではなくデータベースも必要となります。
なので blitz 本体を動かすための node コンテナと、データベースを動かす DB コンテナの二つを用意することにします。
### 2. devcontainer.json と docker-compose.yml を作成して起動する
```json:.devcontainer/devcontainer.json
{
  "name": "blitz",
  "dockerComposeFile": ["./docker-compose.yml"],
  "service": "app",
  "workspaceFolder": "/blitz",
}
```

とりあえずひな形として `devcontainer.json` 作成

```dockerfike:.devcontainer/Dockerfike
FROM mcr.microsoft.com/vscode/devcontainers/javascript-node:0-14
```

あとから拡張しやすいようにメインとなる node コンテナは Dockerfile にするとことにします。
ベースイメージは VSCode を作ってる microsoft が出してる devcontainer 用の node14 イメージを使うことにします。

```yaml:.devcontainer/docker-compose.yml
version: "3"

services:
  node:
    build:
      context: .
      dockerfile: Dockerfile
    working_dir: /blitz
    volumes:
      - ../:/blitz:delegated
      - node_modules:/blitz/node_modules
    tty: true
  db:
    image: mysql:8.0.20
    volumes:
      - mysql-data:/var/lib/mysql
    environment:
      MYSQL_USER: root
      MYSQL_ROOT_PASSWORD: password
volumes:
  node_modules:
  mysql-data:
```
node は作った Dockerfile をビルドし、リポジトリを working_dir である /blitz へマウント。
node なので node_modules は必ずできることがわかっているので named volume にしておきます。
volume についての説明は後述します。

そして DB は mysql の現在の最新を採用。
DB のデータは named volume に設定。enviroment で適当にユーザ名、パスワードを設定しておきます。
開発環境なのでべた書きでもいいかなと思っている派です。

まずこちらで起動してみましょう。
ビルドが完了すると、 

```bash
root ➜ /blitz $ node -v
v14.15.0
root ➜ /blitz $ npm -v
6.14.8
root ➜ /blitz $ yarn -v
1.22.5
```
という結果が取得できるかと思います。

### 3. コンテナ内で開発環境を構築する
新規作成なので [blitz の公式](https://blitzjs.com/docs/getting-started) どおりにインストールしていきます。

```bash
$ npm install -g blitz
$ blitz new .
$ blitz start
~~
ready - started server on http://localhost:3000
```
blitz は node さえあれば立ち上がるようですね。
ここでほかのライブラリが必要であれば apt なりを利用して必要なライブラリをインストールしていき、目当ての開発環境が立ち上がる環境を構築していくことになります。docker とはいえ今回のベースイメージは元をたどれば ubuntu なのでローカルで構築するときとさほど変わりは無いのかなと思います。
しかしこのままの blitz の初期設定では prisma で使う DB が SQLite なのでせっかく用意した mysql は利用されていません。そのための設定変更をしていきます。

```:db/schema.prisma
datasource db {
  provider = "mysql"
  url      = "mysql://root:password@db:3306/blitz"
}
```
ここでのポイントはホストが `db` となっていることです。devcontainer を立ち上げている docker-compose.yml の mysql の service 名は `db` と名付けているので、同じ docker-compose で立ち上げたコンテナ間はデフォルトでサービス名をホスト名にして通信が可能です。
DB の設定変更した後忘れずに `blitz db migrate` を実行しておきましょう

### 4. 各種ファイルを編集する
blitz の開発環境が立ち上がりましたがまだホストからのアクセスができません。docker-compose でポートフォワードの設定をしてあげる必要があります。

```yml:.devconteiner/docker-compose.yml
services:
  app:
    build:
      context: .
      dockerfile: Dockerfile
    volumes:
      - ../:/blitz:delegated
      - node_modules:/blitz/node_modules
    working_dir: /blitz
    tty: true
    ports: # これら追加
      - 3000:3000
```
ports を設定して左下の `><` みたいなマークから `Remote-Containers: Rebuild Container` を実行すると devcontainer の再作成が行われ、新しい設定が反映されます。
再起動が完了したらホスト側のブラウザから localhost:3000 へアクセスしてみましょう。
すると blitz の初期画面が出てくるはずです。もちろん DB との疎通が取れていればユーザ作成もできるはずです。
今回の場合は node さえあれば blitz は動いたので必須なのはポートの設定くらいでしたが、container に apt などを使って何かをインストールしていたらそれを Dockerfile に記述したりする必要があります。
そして後述する `よく使う関連機能 tips` で紹介する devcontainer や docker-compose の便利な機能を追加していきましょう。

# よく使う関連機能 tips
自分が多用したり工夫して使ってる(つもり)のオプションや設定を紹介します。
## devcontainer.json
公式リファレンスは [こちら](https://code.visualstudio.com/docs/remote/devcontainerjson-reference)

- extensions
devcontainer 初回起動時に指定した extension のインストールを行ってくれます。  
あたりまえですが、たくさん指定すると初回起動時にすべてのインストールが走るため、起動は遅くなります。初回だけなのでそんなに気にする必要は無いと思います。  
GROWI では [こんな感じ](https://github.com/weseek/growi/blob/612c88c50e3ef7c387be1a8e762ff6f342433343/.devcontainer/devcontainer.json#L15-L25) に lint ツールや debug ツールなどの extension をデフォルトでインストールさせることで開発環境の統一をより強固な形にしています。
- postCreateCommand  
devcontainer 起動時にコンテナを新規作成した後にコンテナ内で実行させるコマンドを指定することができます。  
コンテナ作成時なので毎回起動時に実行されるわけではないので注意です。  
[このように](https://github.com/haruhikonyan/blitz-first/blob/5315ead3c4bfa89b29c64c41073bc121d0741e76/.devcontainer/devcontainer.json#L12) 指定をすれば、初回起動時に `yarn install` が実行され、node の package が自動的にインストールされます。  
しかし自分のあずかり知らないところで大量のファイルをダウンロードすることになったりするので、好みに合わせて設定しましょう。
- initializeCommand  
devcontainer 起動時にコンテナを新規作成する前に **ホスト側** で実行されるコマンドを設定することができます。  
自分の [試しに作った Blitz.js では](https://github.com/haruhikonyan/blitz-first/blob/5315ead3c4bfa89b29c64c41073bc121d0741e76/.devcontainer/devcontainer.json#L13) サンプルの env ファイルを用意しといてそれをコピーするということを行っています。コピー先は gitignore しているので各々の設定を追加してもリポジトリにコミットされることはありません。

## docker-compose.yml
公式リファレンスは [こちら](https://docs.docker.com/compose/compose-file/)

- [env_file](https://docs.docker.com/compose/compose-file/#env_file)  
ファイル名を指定するとそのファイルに記載された環境変数がコンテナにセットされる。
サンプルの env ファイルをを用意しておいてアクセス token などを開発者に書いてもらったり、前述の initializeCommand と併用してデフォルトの環境変数ファイルを初回にコピーさせたりして使っています。
- [volumes](https://docs.docker.com/compose/compose-file/#volumes)
ものすごく奥が深い設定項目。
    - リポジトリ自体のマウント  
    これをしないと devcontainer 内でファイルの編集ができないのでおそらく必須です。  
    [試しに作ったBlitz.js の devcontainer 用の docker-compose.yml](https://github.com/haruhikonyan/blitz-first/blob/5315ead3c4bfa89b29c64c41073bc121d0741e76/.devcontainer/docker-compose.yml#L9) は .devcontainer に入っているので相対パスとして .. と指定するのがポイントです。  
    また、devcontainer での開発であればファイルの編集はコンテナ内で行うため、ホスト側との整合性を基本的に気にする必要が無いので一番高速である `:delegated` を指定すると良いと思います。

    - 外部ライブラリファイル群を named volume としてマウント  
    node であれば `node_modules`、ruby であれば `bundle`といった外部ライブラリの保存場所を volume として指定してホストとの同期を断ち切ります。基本的にリポジトリには共有をせず直接いじることのいかつ、大量にある外部ライブラリのファイルはリポジトリのデータと一緒にホストと同期してしまうと、ファイルアクセスの度ににホストまで見に行かなくてはならず、スループットが著しく落ちる原因となります。docker for mac の場合はこれが顕著でよく mac の docker は遅いと言われる要因の一つだったりします。
    また、named volume として設定しておくと、コンテナが削除されてもデータは保持し続けられるので、もし開発環境の設定が変わってコンテナを rebuild しなきゃいけなくなった場合もまたライブラリのダウンロードをし直す必要がなくなるので何かと便利であります。  
    [services の volumes](https://github.com/haruhikonyan/blitz-first/blob/5315ead3c4bfa89b29c64c41073bc121d0741e76/.devcontainer/docker-compose.yml#L10) と [root の volumes](https://github.com/haruhikonyan/blitz-first/blob/master/.devcontainer/docker-compose.yml#L25-L26) 両方に書きます。

    - ビルド一時ファイルなどを 名前なし volume としてマウント    
    node であればだいたい dist というディレクトリが作成されてそこに成果物や、開発のために build されたものが一時ファイルとして格納されます。
    理由としては前述したライブラリファイル群と同じく、直接いじることもなければコミットする必要もなく、コンテナ内にさえあれば良いファイルたちなので volume に設定します。
    named である必要がないのは、ライブラリファイル群とは違い、基本的に開発環境を立ち上げるたびに再 build されるものだと思うので保持する必要がないからです。利用しているフレームワークや言語に合わせて named にするかどうかは決めたほうがいいかもしれません。
    Blitz.js の場合ビルドファイルは `.blitz` を生成してそこに格納されるので [このように]
(https://github.com/haruhikonyan/blitz-first/blob/5315ead3c4bfa89b29c64c41073bc121d0741e76/.devcontainer/docker-compose.yml#L11) volume に設定します。 
    こちらは named と違って root の volumes には設定が不要です。

## Dockerfile
公式リファレンスは [こちら](https://docs.docker.com/engine/reference/builder/)

- [FROM](https://docs.docker.com/engine/reference/builder/#from)  
とても奥が深いマルチステージビルドのための設定項目。詳しくは割愛しますが、Rails で webpacker を利用する場合は ruby だけではなく node も同時に必要だったりするので base image は ruby だけども node イメージから node の実行ファイルだけ取得するといったことをやったりします。  
RUN を使って自前でインストールするよりも他 image から持ってくるほうが早くて確実という場合に使うと良いかもしれません。
- [microsoft が用意した devcontainer 用 image](https://hub.docker.com/_/microsoft-vscode-devcontainers)  
直接 Dockerfile 関係ないのですが専用に用意された docker image があるのでこちらを指定して devcontainer を構築すると良いかもしれません。デフォルトで root ではないユーザでコンテナの実行を行ったり、git と連携された PS1 になっていたりいろいろな機能が備わっています。  
なお、タグの指定ですが node14 の場合 `javascript-node:14` と指定をしたくなるのですが、なぜか最新バージョンのものを取得しなかったりするので、明示的に [vscode-dev-containers](https://github.com/microsoft/vscode-dev-containers/releases) のバージョンをを含めた `javascript-node:0-14` このように `0-` を含めたほうが無難みたいです。


## VSCode
- Remote Explorer  
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/100251/e0321e7b-f76e-4ecf-86ea-782385646e45.png)  
VSCode 左側メニューのディスプレイっぽいアイコンを選択するとコンテナの状況などが見られたりします。devcontainer の場合メインのコンテナ一つのコンソールを操作してそこで作業をしますが、時には別で立ち上げたコンテナのログとかを見たい場合があります。そんなときにはここから対象のコンテナを右クリックで選択し `Show Container Log` でログを確認したり、`Attach to Container` で実際にコンソールを取得して操作したりすることができます。

---

※この記事の内容は一部 [WESEEK Tips wiki](https://tips.weseek.co.jp/) に 2020/12/13 に投稿された記事の転載です。
Tips wiki では、IT企業の技術的な情報やプロジェクトの情報を公開可能な範囲で公開してます。