Dockerコンテナを立ち上げただけでフォーマッターの設定を一瞬で完了させられるようにするDev Containersのススメ

[ACCESS Advent Calender](https://qiita.com/advent-calendar/2023/access) 2023年の11日目の記事

---

## 背景
理想(個人的な意見)
- GitとDockerを入れて、git cloneしてdocker-compose buildすれば環境構築完了
- 既存メンバーの環境を新規メンバーが忠実に再現可能

現実
- 色々とごちゃごちゃ入れなければならない
- 他の案件やリポジトリのためにした設定の影響を受ける可能性があり、原因の切り分けも難しい
- 時が経つと、それぞれの人が個人的に入れている拡張機能やbrewやaptで入れているもの、ワークアラウンド対応（一時的な回避策）が満載になり、新規メンバーが同じ環境にするのは無理

→環境構築は辛くて、面倒で思った通りにできない。

### Dockerの限界
Dockerを使っても解決しそうだが、エディタはローカルで操作するため、以下のものは結局手動で入れる必要がある。
- エディタの個人設定（settings.jsonや.vscodeにするような設定）
- エディタの拡張機能
- エディタ操作時に使いたいものでローカルにインストールするもの（静的解析ツールやフォーマッター、デバッガー）
- 上記のパスの設定

→必要なものをコンテナ内に全部入れてDev Containersで作業すれば良いのでは？

## Dev Containersとは
- Visual Studio Codeの拡張機能の一つ([Link](https://code.visualstudio.com/docs/devcontainers/containers))
- ローカルで操作しているかのような感覚でコンテナ内で作業が可能
![](https://code.visualstudio.com/assets/docs/devcontainers/containers/architecture-containers.png)
- 使い方は[公式チュートリアル](https://code.visualstudio.com/docs/devcontainers/tutorial)を見てもらう方が早いので、この記事では説明しない

## Dev Containersを使うメリット
### メリットその1: フォーマッターやリンターの統一
- フォーマッターやリンターのバージョンを統一できる
- Dev Containersを使うとローカル環境と切り離せるので、気軽にFormat on SaveをONにできる
  - 案件を複数掛け持ちしていると、案件によってはFormat on Save出来ないことがある(例：フォーマッターが未導入なので、下手にONにすると差分が大量に出る状況)

### メリットその2: 拡張機能の統一
- .vscode/extension.jsonに書くよりも確実に統一化できる
- コンテナに入った段階でインストール済みの状態になる
- 必要であれば、ローカルのものをコンテナ内にインストールすることも可能

## Dev Containersを使うデメリット
- 個人的に追加したい設定がやりづらい
  - 例: GitHub Copilot使いたいor使いたくない
  - 対応策としては、.vscode/setting.jsonを自分で用意し、それをgitの追跡外にする
- vscodeでチームを統一化する必要がある
  - 他のエディタ、IDEを使う人を改宗させる必要がある
- 初回起動に時間がかかる
  - 拡張のインストールやentrypoint.shの処理
- 少しもっさりすることがある
  - コンテナ内なので多少はしょうがない
- gitの設定を意識する必要がある
  - 楽なのは~/.sshや~/.gitconfigもマウント

## 個人的に使っているdevcontainer.jsonの設定例の紹介
ここでは、趣味プログラミングで使っている設定を紹介する。

### Reactでの設定例
- Reactでフォーマッターや静的解析ツールとして、Prettier、ESLint、CSScombを利用している際の設定
- 開発メンバーが各々で.vscodeに書くような設定やインストールする必要がある拡張機能を設定可能
- .eslintrc.jsonや.prettierrc.jsonがあれば、Dev Containersを立ち上げた段階で、フォーマッターや静的解析ツールが動かせるようになる

（あくまで参考例なので、環境に合わせて、適宜修正が必要）
```javascript
{
  "name": "Frontend Dev Environment",
  "dockerComposeFile": "./docker-compose.yml",
  "service": "frontend",
  "workspaceFolder": "/app",
  "forwardPorts": [8000],
  "postCreateCommand": "echo 'Frontend container is ready!'",
  "customizations": {
    "vscode": {
      "extensions": [
        // 汎用的な拡張機能
        "yzhang.markdown-all-in-one",
        "jebbs.plantuml",
        "Gruntfuggly.todo-tree",
        "chihiro718.whitespacepp",
        "oderwat.indent-rainbow",
        // フロントエンド開発に必要な拡張機能
        "esbenp.prettier-vscode",
        "dbaeumer.vscode-eslint",
        "stylelint.vscode-stylelint",
        "mrmlnc.vscode-csscomb",
        "mike-co.import-sorter"
      ],
      "settings": {
        // エディター全般の設定
        "editor.tabSize": 2,
        "editor.formatOnSave": true,
        "editor.defaultFormatter": "esbenp.prettier-vscode",
        "editor.codeActionsOnSave": {
          "source.fixAll": true
        },
        "editor.rulers": [127],
        "editor.guides.bracketPairs": true,
        "files.insertFinalNewline": true,
        // 特定の言語向けのエディタ設定
        "[css]": {
          "editor.formatOnSave": false
        },
        "[scss]": {
          "editor.formatOnSave": false
        },
        "[markdown]": {
          "editor.formatOnSave": false
        },
        "[jsonl]": {
          "editor.formatOnSave": false
        },
        // Prettierの設定
        "prettier.singleQuote": true,
        // ESLintの設定
        "eslint.format.enable": true,
        "eslint.lintTask.enable": true,
        "eslint.debug": true,
        // CSScombの設定
        "csscomb.ignoreFilesOnSave": [],
        "csscomb.preset": "../.vscode/csscomb.json",
        "csscomb.syntaxAssociations": {
          "postcss": "scss"
        },
        "csscomb.formatOnSave": true,
        // CSS/SCSSの検証設定
        "css.validate": false,
        "scss.validate": false,
      }
    }
  }
}
```

### Railsでの設定例
- こちらのファイルでは、Railsでよく使われているRubocopの設定とReactでも使っている汎用的な設定をしている
- 「rubocop関連の設定」ではパスの指定を絶対パスで行っており、そうしても問題ないことが多い
  (コンテナ内でのパスなので、人によってパスが変わることがない)

```javascript
{
  "name": "Backend Dev Environment",
  "dockerComposeFile": "./docker-compose.yml",
  "service": "backend",
  "workspaceFolder": "/app",
  "customizations": {
    "vscode": {
      "extensions": [
        // 汎用的な拡張機能
        "yzhang.markdown-all-in-one",
        "jebbs.plantuml",
        "Gruntfuggly.todo-tree",
        "chihiro718.whitespacepp",
        "oderwat.indent-rainbow",
        // バックエンド開発に必要な拡張機能
        "rebornix.ruby",
        "wingrunr21.vscode-ruby",
        "misogi.ruby-rubocop",
        "castwide.solargraph"
      ],
      "settings": {
        // エディター全般の設定
        "editor.tabSize": 2,
        "editor.formatOnSave": true,
        "editor.codeActionsOnSave": {
          "source.fixAll": true
        },
        "editor.rulers": [
          127
        ],
        "editor.guides.bracketPairs": true,
        "files.insertFinalNewline": true,
        // rubocop関連の設定
        "ruby.rubocop.configFilePath": "/app/backend/.rubocop.yml",
        "ruby.rubocop.executePath": "/usr/local/bundle/bin/",
        "ruby.format": "rubocop",
        "[ruby]": {
          "editor.defaultFormatter": "misogi.ruby-rubocop"
        }
      }
    }
  },
  "forwardPorts": [
    3000
  ],
  "postCreateCommand": "echo 'Backend container is ready!'"
}
```

## 感想
- 開発環境はできるだけ簡単&確実に統一したい→そんなあなたにDev Containers
- Dev Containersを使えば、「俺or私が考えた最強のエディタ」を気軽に共有できる状況になる
- 設定ファイルに追記してプルリクエストを作成すれば、みんな幸せになる