開発環境の構築で疲弊してる？VSCode DevContainersで開発しよう！

本記事は、[エムスリーキャリア Advent Calendar 2022](https://qiita.com/advent-calendar/2022/m3c)の7日目の記事です。

2022年ということで「x86/ARM混成チームで辛いならDevContainers使おうぜ」という切り口で執筆していますが、x86の方々もDevContainersはメリットいっぱいですので是非お試しください！

# 2022年の開発環境事情
エムスリーキャリアのWebエンジニアは支給される開発マシンをWindowsノートPCかMacBook Proから選択することができます。
また本番環境で動くRailsやSpringBoot等はLinuxがインストールされたサーバーにデプロイしています。

つまり各環境で動いているOSの種類がWindows・macOS・Linuxとバラバラで、この差を埋めるために各チームで工夫を凝らしてきました。
とはいえコンピュータアーキテクチャはインテルとAMDが採用しているx86 64bit(以下、`x86_64`)で統一されており、環境差で開発が出来ないというような致命的な問題ありませんでした。

しかし2022年、M1 Macを始め**ARMアーキテクチャを搭載したPCが普及し、開発者たちは頭を抱えることになるのであった**−−というお話。

## 新入社員あるある「libv8, node-sassが動かないんすけど？」
2021年10月にIntel搭載MacBook Proの出荷が終了したことで、エムスリーキャリアでも新入社員を皮切りにAppleシリコンMacを導入することになりました。

業務で利用するソフトウェアのほとんどは問題なく動きましたが、開発プロジェクトの依存関係がインストールできなかったり、うまく動作しなかったりする問題が発生。
具体的には、Ruby on Railsで開発していると高確率で以下のライブラリがコケます。
* bundler
    * `libv8` <`therubyracer`,  `mini_racer`@0.3.1以下が依存
* yarn
    * `node-sass` < `@rails/webpacker`@5.2.1以下が依存

これらがAppleシリコンMacで動かない件は、依存関係を修正することで解決できます。

依存関係の修正方法
```md
`therubyracer`は2017年を最後に更新されておらず、また`mini_racer`も環境依存でインストールに失敗しやすい`libv8`をやめて0.4.0から`libv8-node`を使うようになったため、`mini_racer`0.4.0以上に移行することで解決します。
また、RubyからJavaScriptをコールする`execjs`にはインストールされているJSランタイムを自動的に選択する仕組みがあり、Webpackerの導入等でNode.jsを入れている場合は`therubyracer`も`mini_racer`も不要になっているケースがほとんどです。

`node-sass`は(内部で利用している`LibSass`ごと)非推奨となり、`DartSass`を利用する`sass`(紛らわしいですがパッケージ名です)への移行が推奨されています。
`node-sass`に依存していた`@rails/webpacker@4`も、5.2.2以降に更新することで`sass`を利用するようになります。
```

日頃からRailsのトレンドにアンテナを張って、依存関係を適切に修正していればこのような問題は回避できますが、それが難しいプロジェクトがあるのも事実です。
その場合、
1. **本番環境ごと古い依存関係を修正する**(それに伴う影響範囲も動作確認・修正する)
1. または、**本番環境との差異を覚悟した上で開発環境のみ修正する**
 
の難しい選択を新入社員とその配属チームに強いることになりました。
そこで第3の選択肢「**x86前提の開発環境をコンテナ化してDev Containersで開発する**」を提案します。[^1]
[^1]:後述の通りアーキテクチャエミュレーションを行うため、パフォーマンスの低下・予期せぬ誤動作が発生する可能性があります。あくまで選択肢の一つであり、工数と現場の理解さえあれば1.**本番環境ごと古い依存関係を修正する**がベストな選択肢であることを強調しておきます

# Dockerにはアーキテクチャエミュレーション機能があるよ
普段何気なくDockerを使っていると気づきにくいですが、Dockerにはホストマシンとコンテナとのアーキテクチャ差異を吸収するエミュレーション機能があります。
WindowsやmacOSでDocker Desktopを利用している人は自動的に機能オンになっています。

以下のコマンドでアーキテクチャエミュレーションが動作しているか確認できます。
```shell
# linux/amd64のalpineイメージをpull
$ docker pull --platform=linux/amd64 alpine
# alpineイメージで`arch`コマンドを実行
$ docker run --rm alpine arch
> x86_64    # x86_64で動作している

# linux/arm64のalpineイメージをpull
$ docker pull --platform=linux/arm64 alpine
# alpineイメージで`arch`コマンドを実行
$ docker run --rm alpine arch
> aarch64   # ARMで動作している
```

ちなみに`--platform`を省略した場合は、ネイティブで動作するイメージが自動的にpullされます。[^2] [^3]
[^2]:[Dockerマニフェストリスト](https://docs.docker.jp/engine/reference/commandline/manifest.html)が存在する場合のみ。自前でビルド・プッシュしているイメージはマニフェストがないケースがほとんどです
[^3]:ネイティブで動作するイメージが登録されていない場合はエラーになります
```shell
# Intel Macでの実行例

# --platformを省略してalpineイメージをpull
$ docker pull alpine
# alpineイメージで`arch`コマンドを実行
$ docker run --rm alpine arch
> x86_64    # x86_64で動作している
```

# 【ここから本題】x86前提の開発環境をコンテナ化してDev Containersで開発する
## Dev Containersとは
[Dev Containers](https://marketplace.visualstudio.com/items?itemName=ms-vscode-remote.remote-containers)とは、Visual Studio Codeの開発元であるMicrosoft公式が提供する拡張機能です。

まるでDockerコンテナ上にVSCodeをインストールしたかのように、VSCodeの機能を使ってコンテナ上で開発できるようになります。
具体的には、VSCodeの以下機能がコンテナで利用できます！
* **コードエディターとしての全機能**
* **ソース管理(何とホストマシンのgit設定が自動的に反映されます！SUGOI！)**
* **統合ターミナル**
* **デバッガー実行**
* **豊富な拡張機能**

## 前提条件
以下をインストールしていることが前提です。
* Docker Desktop (v4.12.0で確認)
* Visual Studio Code
* 拡張機能：[Dev Containers](https://marketplace.visualstudio.com/items?itemName=ms-vscode-remote.remote-containers)

## 説明用に空のRailsプロジェクトを用意しました
手元にRailsプロジェクトがない人もいるかと思いますので、空のRailsプロジェクトを用意しました。
実際の開発現場でもありそうな少し古い環境を想定して作成しました。
* ruby 2.7.7
* rails 6.0.6
* sprockets 4.1.1
* node.js 14.21.1
* webpacker 4.0.3
* ARM上のインストールに問題のある、mini_racer@0.3.1, @rails/webpacker@4.0.3を使用
* レガシーなJSはsprockets・uglifierで、Vue.jsを使うなど新しいJSはwebpackerでプリコンパイル
* 説明簡略化のためDBが必要なActive Recordは未使用

https://github.com/morooka-cube/rails_dev_container_example

以下のコマンドでクローンしてお使いください。
これ以降、VSCodeで`rails_dev_container_example`を開いた状態で説明します。
```shell
git clone https://github.com/morooka-cube/rails_dev_container_example.git
```

## Dev ContainersでRails開発を始める
ここでは各種設定ファイルを作成してRails開発を始める方法を説明します。[^4]
[^4]: 実のところDev Containersはウォークスルー方式でセットアップすることもできますが、既存のプロジェクトをDev Container化するにはいささか冗長に感じました

### 設定ファイルの作成
まず`.devcontainer`フォルダを作成し、コンテナの素材となる`.devcontainer/Dockerfile`を作成します。
内容を簡単に説明すると、以下3つのビルドステージに分けて最終的に3.を出力しています。
1. `node.js`と`yarn`の実行ファイルを取得
1. `yarn install`を実行
1. `bundle install`を実行し、1.から`node.js`と`yarn`を、2.から`yarn install`の結果をコピー

```Dockerfile:.devcontainer/Dockerfile
# --- node.jsとyarnの実行ファイルが入ったイメージ ---
FROM node:14.21.1-slim AS node

# --- yarn installを行うビルドステージ ここから
FROM node AS yarn

# ワーキングディレクトリを設定
WORKDIR /app

# ビルドコンテキストからyarn installに必要なファイルをコピー
COPY package.json yarn.lock /app/
# yarn installを実行
RUN yarn install
# --- yarn installを行うビルドステージ ここまで

# --- 最終的なビルド結果となるステージ ここから
FROM ruby:2.7.7

# nodeからnode.jsとyarnの実行ファイルをコピー
COPY --from=node /usr/local/bin/node /usr/local/bin/node
COPY --from=node /usr/local/include/node /usr/local/include/node
COPY --from=node /usr/local/bin/yarn /usr/local/bin/yarn
COPY --from=node /usr/local/bin/yarnpkg /usr/local/bin/yarnpkg
COPY --from=node /opt/yarn-v1.22.19 /opt/yarn-v1.22.19

RUN \
  # nodeとyarnが実行できるようシンボリックリンクを設定
  ln -s /usr/local/bin/node /usr/local/bin/nodejs && \
  ln -s /opt/yarn-v1.22.19/bin/yarn.js /usr/local/bin/yarn.js

# ワーキングディレクトリを設定
WORKDIR /app 

# ビルドコンテキストからbundle installに必要なファイルをコピー
COPY Gemfile Gemfile.lock /app/
# bundle installを実行
RUN bundle install

# yarn install結果をコピー
COPY --from=yarn /app /app

# ポート3000番に穴を開ける
EXPOSE 3000
# --- 最終的なビルド結果となるステージ ここまで
```
2つ目に、コンテナの起動方法を記述する`.devcontainer/docker-compose.yml`を作成します。
```yml:.devcontainer/docker-compose.yml
services:
  rails:
    build:
      # プロジェクト全体をビルドコンテキストに設定
      context: ../
      dockerfile: .devcontainer/Dockerfile
    # x86_64用を指定してビルド(amd64なのは多分大人の都合...)
    platform: linux/amd64
    image: rails_dev_container_example
    volumes:
      # プロジェクト全体を/appにマウント
      - ../:/app
      # /app/node_modulesのみビルド時のものを使用(volume trick)
      - /app/node_modules
    ports:
    # コンテナの3000をホストの3000にフォワード
      - 3000:3000
    # 何も実行しない状態でコンテナを待機させる
    command: sleep infinity
```
最後に、Dev Containerの設定ファイル`.devcontainer/devcontainer.json`を作成します。
```json-doc:.devcontainer/devcontainer.json
{
	"name": "Rails on x86",
	"dockerComposeFile": "docker-compose.yml",
	// VSCodeで接続するコンテナ
	"service": "rails",
	// VSCodeで開くフォルダ
	"workspaceFolder": "/app"
}
```
ここまでで、`.devcontainer`上に3つのファイルが保存されているはずです。
![スクリーンショット 2022-12-06 17.35.31.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2986436/507f6a35-dc8d-224f-aca3-6ec05c6a5436.png)

### Dev Containerの起動
それでは早速Dev Containerを立ち上げてみましょう！
VSCodeで`Command`+`Shift`+`P`のショートカットでコマンドパレットを開き、`Dev Containers:
Reopen in Container`を選択します。
 ![スクリーンショット 2022-12-06 17.46.04.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2986436/aa37c794-5118-baff-c418-551d2680b5a0.png)
するとコンテナのビルドなどが行われ(結構長い)、Dev Containerが起動します！
![スクリーンショット 2022-12-06 17.54.04.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2986436/33dc655f-c84a-bf1f-cb66-47fd74307db0.png)
アーキテクチャシミュレーションのバグ？でコード変更監視がエラーになるため、`/config/environments/development.rb`を修正します。
```diff_ruby:/config/environments/development.rb
- config.file_watcher = ActiveSupport::EventedFileUpdateChecker
+ config.file_watcher = ActiveSupport::FileUpdateChecker
```
Dev Container上でターミナルを開き、以下コマンド実行後ブラウザで`http://localhost:3000`にアクセスして、Rails Serverが起動していることを確認してください。
```shell
$ rails server -b 0.0.0.0 -p 3000
```
![スクリーンショット 2022-12-06 18.03.00.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2986436/75bfcc74-9436-282b-affd-7fceb1a7f09d.png)

## `webpack-dev-server`も同時に起動する
ここまででDev Containerを使ってRailsの開発環境を構築することが出来ましたが、Dev Containerの真の姿はまだこれからです。

Webpackerを使う開発のベストプラクティスとして、「`webpack-dev-server`を利用してホットリロードを有効にする」というものがあります。
今の状態でも、ターミナルを２枚立ち上げて片方で`$ bin/webpack-dev-server`を起動する手段が使えますが、毎回必要なので面倒ですし忘れてしまいそうです。

`docker-compose.yml`を修正して`webpack-dev-server`を立ち上げるよう設定しましょう。
```diff_yaml:.devcontainer/docker-compose.yml
services:
  rails:
# ...略
    ports:
    # コンテナを3000をホストの3000にフォワード
      - 3000:3000
+   environment:
+     # webpackコンテナで実行しているwebpack-dev-serverを見にいく
+     - WEBPACKER_DEV_SERVER_HOST=webpack
    # 何も実行しない状態でコンテナを待機させる
    command: sleep infinity
+ webpack:
+   image: rails_dev_container_example
+   volumes:
+     - ../:/app
+     - /app/node_modules
+   ports:
+     - 3035:3035
+   environment:
+     - WEBPACKER_DEV_SERVER_HOST=0.0.0.0
+   # webpack-dev-serverを実行する
+   command: bin/webpack-dev-server
```
変更後、コマンドパレットで`Dev Containers: Rebuild container`を選択します。
![スクリーンショット 2022-12-06 18.19.28.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2986436/20efc827-0406-91b0-9ca0-d5be4d27b0dd.png)
これでDev Containerとは別のコンテナで、`webpack-dev-server`が起動するようになりました！

## VSCodeのデバッガー実行・拡張機能を有効にする
VSCodeは優れたコードエディタであると共に、豊富な拡張機能を利用することでIDEにも引けを取らない強力なデバッガーとなります。

Dev Containerはインストールする拡張機能を`devcontainer.json`に記述するので、この設定ファイルをチームで共有することでメンバー全員が同じ拡張機能を使うことが出来ます！

それでは`Dockerfile`に手を加えて、デバッガーに必要なGemをインストールしましょう。
```diff_docker:.devcontainer/Dockerfile
# ...略
RUN \
  # nodeとyarnが実行できるようシンボリックリンクを設定
  ln -s /usr/local/bin/node /usr/local/bin/nodejs && \
  ln -s /opt/yarn-v1.22.19/bin/yarn.js /usr/local/bin/yarn.js && \
+ # デバッグ用のGemをインストール
+ gem install solargraph ruby-debug-ide debase
# ...略
```

次に`devcontainer.json`を編集して、インストールする拡張機能を指定します。
```diff_jsondoc:.devcontainer/devcontainer.json
{
	"name": "Rails on x86",
	"dockerComposeFile": "docker-compose.yml",
	// VSCodeで接続するコンテナ
	"service": "rails",
	// VSCodeで開くフォルダ
-	"workspaceFolder": "/app"
+	"workspaceFolder": "/app",
+	"customizations": {
+		"vscode": {
+			"extensions": [
+				"endverbraucher.pack-rails",
+				"castwide.solargraph",
+				"Vue.volar",
+				"walkme.Ruby-extension-pack"
+			]
+		}
+	}
}
```

最後にデバッガーの実行設定を新しいファイル`.vscode/launch.json`に記載します。
```json:.vscode/launch.json
{
  "version": "0.2.0",
  "configurations": [
    {
      "name": "Rails server",
      "type": "Ruby",
      "request": "launch",
      "program": "${workspaceRoot}/bin/rails",
      "args": [
        "server", "-b",  "0.0.0.0", "-p", "3000"
      ]
    }
  ]
}
```

コマンドパレットでDev Containers: Rebuild containerを選択し、Dev Containerが起動したら`F5`キーを押してデバッガーを実行します。
![スクリーンショット 2022-12-06 19.13.10.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2986436/ac9bd396-61b3-4ea2-a6d6-09b553cacc1a.png)

これでDev Container上でRailsをデバッグすることが出来ました！

# 謝辞
同僚の@akitoshigaさんにAppleシリコン搭載Macでの動作確認を行ってもらいました！
彼は[4日目](https://qiita.com/akitoshiga/items/3cba081f6311d09d5485)と20日目も担当しております。

なお４日目を見て分かる通り彼はRubyMine派ですが、Dev Containerのデバッグ画面を見てVSCodeへの乗り換えを検討したいと言うほど感動していました :thumbsup: 

# 終わりに
エムスリーキャリアでは積極的にエンジニアを採用しております。
Dockerなど新めの技術を取り入れた開発環境で医療従事者の働き方を革新しませんか？

Appleシリコン搭載Macによる開発体制も整っておりますのでApple派の方も安心です。
*US配列も選べるよ！*

https://career.m3career.com/midcareer/engineer