【Docker】Remote-Developmentの構築

## 概要
WSL2で作ったDockerfileやdocker-compose.ymlをベースに、コンテナ内にアタッチした状態（コンテナに入ってターミナルを開いた状態）で開発を行える仕組み。厳密にはDocker標準の機能ではなく、VSCodeの拡張機能「Remote Development」によって実現されています。

| 項目 | 内容 |
| ---- | ---- |
| **取り扱う内容** | • VS Code Remote Developmentの基本的な仕組みと設定方法<br>• Remote-Developmentを利用するメリット<br>• docker-compose.yml、devcontainer.jsonの代表的な設定項目<br>• トラブルシューティング |
| **想定読者** | • WSL2環境でDockerを使用している開発者<br>• VS Codeでコンテナ開発を始めようとしている人<br>• Remote Developmentに絡んでエラーが出て困っている人 |

## Remote-Developmentを利用するメリット
通常のDocker環境では、コンテナ内のリソースを利用するために、まずコンテナにアタッチする必要があります。VS CodeのRemote Developmentを使用すると、任意のコンテナにアタッチした状態で開発を開始できるため、以下の機能が直接利用可能になります。
* コンテナ内でのみ利用可能なパッケージやライブラリへの参照
Remote Developmentは、コンテナにアタッチした状態でVSCodeを開きます。これにより、VSCodeの実行環境がホストではなくコンテナ内に移動されます。その結果、コンテナ内にインストールされたパッケージやライブラリを、VSCode上で直接参照・利用することが可能になります。具体的には、コマンド実行やコード入力時の補完、定義ジャンプといった機能を利用する際に、パッケージやライブラリを利用しながら最大限活用できます。
* コンテナ内で実行するサービスのデバッグ
Remote Developmentを使用すると、VSCodeのデバッガーをコンテナ内で実行されているプロセスに直接アタッチできます。これにより、ブレークポイントの設定、変数の監視、ステップ実行などのデバッグ機能を、あたかもローカル環境で開発しているかのように利用できます。

## 導入手順
VSCodeの拡張機能`Remote Development`をインストールします。

VSCodeでコマンドパレットから`Reopen in Container`すします。初めて実行する場合など、いずれの設定ファイルに基づいてコンテナを起動するか問われる場合があります。`docker-compose.yml`が既にある場合、`From docker-compose.yml'を選びます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3906608/7e47d4d7-5da8-ff6f-2c4d-1aaeddecb91c.png)

追加機能をインストールする画面が出た場合、いずれにもチェックを入れずに「OK」をクリックします。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3906608/f291b8af-178d-1965-951f-551c46616261.png)

WSL2側で開いていたプロジェクトのルートディレクトリに`.devcontainer`ディレクトリが自動作成されます。中身は`docker-compose.yml`と`devcontainer.json`の2ファイル。既にファイルがある場合は、既存ファイルの内容にしたがってコンテナ起動されます。

この時点でのWSL2側ディレクトリ構成は、以下例のようになっています。
```txt
your-project/
├── .devcontainer/ (Remote-Developmentの設定ファイル Reopen in Container時に自動生成)
│   ├── devcontainer.json
│   └── docker-compose.yml
├── frontend/
│   ├── Dockerfile
│   ├── src/
│   ├── package.json
│   └── ... (その他のフロントエンド関連ファイル)
├── backend/
│   ├── Dockerfile
│   ├── src/
│   ├── package.json
│   └── ... (その他のバックエンド関連ファイル)
├── db/
│   └── data/ (データベース関連ファイル、ボリュームマウント用)
├── docker-compose.yml
└── README.md
```

この構成では、コンテナを起動する方法によって、適用される設定が以下の通りに変わります。

* 通常のdocker-composeコマンドでコンテナを起動する場合
    * プロジェクトルート、つまり`your-project/`で`docker-compose up`した場合を指す
    * プロジェクトルートの`docker-compose.yml`にしたがってコンテナが起動される
*  VSCodeの`Reopen in Container`を使用する場合
以下すべてを適用してコンテナが起動されます。
    * `.devcontainer/devcontainer.json`から、VSCode固有の設定（使用する拡張機能、コンテナ起動後のコマンドなど）が適用される。
    * プロジェクトルートの`docker-compose.yml`が基本設定として読み込まれる
    * `.devcontainer/docker-compose.yml`の設定が、上書きまたは追加される

## 設定項目
特に重要だと感じた項目について記載します。

### .devcontainer.json
VSCodeがコンテナ内でどのように動作するかを定義する設定ファイル。
* service
コンテナ起動時にアタッチするサービスを指定します。`docker-compose.yml`の`services`に記載した名前を指定します。すべてのコンテナを起動した上で、VSCodeのウィンドウおよびターミナルが、ここで指定したコンテナ内で開きます。
    ```json
    "service": "frontend",
    ```

ここで指定しなかったサービスも、コンテナ自体は起動していますので、そちらの開発や操作を行う場合は、後述の「別のコンテナにアタッチする」を行って別コンテナへアタッチしてください。

### docker-compose.yml
ルートディレクトリの`docker-compose.yml`に対して、追加または上書きする設定を保持します。開発環境でだけ適用したい設定があれば、ここに記載するとよいです。ルートディレクトリの`docker-compose.yml`に記載済の設定は引き継がれるので、ここには追加・上書きが必要な設定だけ記載すればよいです。
* volumes
    ```yaml
    # ルートディレクトリをコンテナ内の`/workspace`にマウントする。`:cached`はパフォーマンス向上のために使用される
    - ..:/workspaces:cached
    ```
* command
    コンテナ起動時に実行されるコマンド。自動生成された`.devcontainer/docker-compose.yml`では以下のコマンドが自動で書き込まれているはず。
    
    ```yaml
    # コンテナ内でsleepコマンドを走らせて、コンテナがすぐに終了するのを防ぐ
    command: /bin/sh -c "while sleep 1000; do :; done"
    ```

   この設定は、 Dockerfileの`CMD`やdocker-compose.ymlの`command`よりも優先されるので注意しましょう。（例: Dockerfileの`CMD`に`npm run start`を記載してサービスが自動起動するようにしていても、上記sleepコマンドが優先で実行されて、Dockerfile側の`npm run start`は実行されない。この場合、yaml側のcommandをコメントアウトするなどして対応します）

## 操作Tips
### 別のコンテナにアタッチする
VSCodeのコマンドパレットから`Attach to Running Container`を実行し、表示されるリストからアタッチしたいコンテナを選択すると、別コンテナのターミナルを別ウィンドウで開くことができます。

使用シーン：
* アタッチ中のコンテナ以外のコンテナでコマンドを実行する必要がある場合
* 複数のコンテナを同時に操作したい場合
* デバッグや監視のために別コンテナの状態を確認したい場合

## トラブルシューティング
### Dockerfileに加えた変更が反映されない
コンテナの起動時、VSCodeのコマンドパレットから`Reopen in Container`ではなく`Rebuild and Reopen in Container Without Cache`してみましょう。コンテナの再ビルドが走るので、変更後のDockerfileに基づいてコンテナが実行されます。

### Resolver error: Error: Server returned 404エラー
コンテナ起動時、上記エラーが発生したことがあります。VSCodeのバージョンが古かったのでアップデートしたら解消しました。

VSCodeバージョン：1.87.2 → 1.91.1 にアップデート。
Remote Development拡張機能バージョン：0.25.0

### コンテナ内でGitが利用できない
#### `Reopen in Container`をやり直す
端末や環境によるのかもしれないですが、`Reopen in Container`したままPCをスリープ→復帰すると、その後のgit操作が以下エラーによって通らなくなることがありました。再度`Reopen in Container`すると解消します。コンテナの再ビルドまでは不要で、入り直しさえすればOK。
```bash
# 上記該当時のエラー
git@github.com: Permission denied (publickey).
fatal: Could not read from remote repository.
```

#### GitHub SSH関連の設定が完了しているか確認する
[【Git】WSL2環境でGitを使う場合のGitHub SSHキー設定](https://qiita.com/Shimooo_mutenka/items/898bcf9891f47f05aca7)に記載のSSH Agent設定を行っていない場合、実施します。

#### WSL2の`gitconfig`や`.ssh`をマウントしているなら、無効化する
`.devcontainer/docker-compose.yml`などDocker関連の設定ファイルで、SSH関連のファイルをコンテナへマウントしている場合、これを解除します。

```yaml
# 例：volumesの箇所
# ルートディレクトリをコンテナ内の`/workspace`にマウントする。`:cached`はパフォーマンス向上のために使用される
- ..:/workspaces:cached
# 以下はコメントアウトする
# ~/.gitconfig:/home/node/gitconfig
# ~/.ssh:/home/node/.ssh
```

そもそも、ホスト側のSSH関連ファイルをコンテナ内に持ち込むのは以下の観点から非推奨と思っています。
* ホストとコンテナでユーザーが異なる場合に、マウントされたファイルやディレクトリのアクセス権が書き変わったり、アクセス不可になったりする。コンテナ内で無理やり権限を書き換えて、他のコンテナでそのファイルが読めない事態に陥りがち。
* セキュリティ上、秘密鍵情報がコンテナ内にファイルとして存在している状態は、機密漏洩のリスクにつながる。keychainはメモリ経由で情報が渡るので、鍵の情報が洩れる心配が少ない。

ただし、アサインされたプロジェクトで上記マウントによる方法を採用していたら、チームの方針に従ってください。

#### Manage Unsafe Repositories ボタンが表示される
[Remote Developmentで「Manage Unsafe Repositories」が表示される](https://qiita.com/Shimooo_mutenka/items/d0177e8172628be8c987)を参照して

### `CMD`や`command`が無効になる
[設定項目](#設定項目)に記載のとおり、`.devcontainer/docker-compose.yml`の`command`が有効になっている場合、Dockerfileの`CMD`やdocker-compose.ymlの`command`が上書きされて無効化されてしまいます。`.devcontainer/docker-compose.yml`の`command`をコメントアウトして、再度コンテナを起動すると解消します。

### 特定のコマンドが実行できない
特定のコマンドを実行しようとした際に、`command not found`などといってコマンドが実行できない場合、以下を確認してください。

例：
* This command is not available when running the Angular CLI outside a workspace.（Angular関連のコマンド）

#### 対象のパッケージがインストールされているか確認する
コンテナのビルド後など、プロジェクトにおいて`npm install`や`pip install`といったパッケージ/ライブラリ追加プロセスが自動化されていない場合、コンテナ起動時点ではまだパッケージがインストールされていない可能性があります。

コンテナ起動時点でパッケージインストールが完了しているか確認し、自動インストール処理があればそちらの復旧を行います。自動でインストールする仕組みが無ければ、手動でインストールを行います。

#### 実行したコマンドがアタッチしていない別コンテナのものでないか確認する
ルートディレクトリ以下すべてのソースコードをマウントしている場合などに混乱しがちだが、`Reopen in Container`した際に開いているVSCodeウィンドウやターミナルは、`.devcontainer.json`で指定したコンテナ内で開いています。

したがって、仮にフロントエンドのコンテナにアタッチしている場合、バックエンドやデータベースといった別コンテナで動作する前提のコマンドは、アタッチ中のコンテナでは通りません。[別のコンテナにアタッチする](#別のコンテナにアタッチする)を参照して、正しいコンテナのウィンドウを開いて実行してください。