devcontainerの運用ベストプラクティス

# Devcontainerとは？

こちらの記事が参考になる

https://qiita.com/yoshii0110/items/c480e98cfe981e36dd56

# この機能を使う目的

- devcontainerはコンテナ内で環境を作成できるので、「作業者の環境にパッケージなどが依存しており、本番環境で動かない！」ということがない
- いちいち`docker run`して`docker attach` してdockerコンテナ内に入る、という作業がなくなる
- Git, Githubの設定をホストマシンから引き継いで、Githubにプッシュできる
- Dockerコンテナの起動方法を記述して、開発環境に特化させた環境を記述できる。例えば、開発環境のみに必要なGitや開発依存のnpmパッケージをインストールする記述が可能

# 現状の運用最適解

- 環境差異をなくすため。devのための環境はできるかぎり作らない。その環境差異はdevcontainerに吸収させる。

- onCreateCommandでgit, openssh-client、npmの開発依存packageなどのインストール。
    Git, openssh-clientをインストールするのは、コンテナ内でGithubにプッシュするため。

- postStartCommandで開発用の永続プロセス（サーバーなど）を起動

さらに便利にするなら

- 実行中のコンテナでも、サーバーを起動してdevcontainerを立ち上げたい。しかし、Attachする前に起動された永続プロセスが起動の邪魔になる。
→なので、postAttachCommandで以前に実行されたサーバーkillをするコマンド＋永続プロセス（サーバーなど）を追記。以下はpythonのuvicornの場合。
```bash
"kill $(ps aux | grep 'uvicorn' | grep -v 'grep' | awk '{print $2}') || true && sleep 1 && DEBUG=1 python3 -m debugpy --listen 0.0.0.0:5679 -m uvicorn  app:app --host '0.0.0.0' --port 8080 --reload"
```

- postStartCommandではなく、postAttachCommandでVSCodeのコマンド **「実行中のコンテナにアタッチ」** でも動作する上位互換コマンドだから。

# devcontainer.jsonファイルの書き方

- プロジェクトのルートディレクトリに`.devcontainer/devcontainer.json`を用意する
- 自分のPCの環境をホスト(`host`)、もしくはローカル(`local`)と呼ぶ
- コンテナ内の環境を、コンテナ(`Container`)、もしくはリモート(`remote`)と呼ぶ

## dockerファイルを使用する場合
重要な要素のみ解説します。

### name

VSCodeの上部の枠に表示される名前（コンテナ名ではないので注意）

#### build/image

dockerfileを指定するパターンとimageを指定するパターンがある。buildとimageは並列できない。

```json
"build" : {
    "dockerfile": "../Dockerfile",
    "context": "./"
    "target": "development"
  }
```

もしくは
```json
"image": "YOUR DOCKER IMAGE NAME"
```


### runArgs

docker runで指定する引数を指定できる。-vや-p相当のものは用意されているので、この設定ファイルでカバーしてない引数を指定する。例えば、コンテナの名前を定義する`—-name` や終了後コンテナを削除する`—-rm`などが候補である。

```json
  "runArgs": [
    "--name=CONTAINER_NAME",
    "--rm"
  ]
```

### workspaceFolder

`“workspaceFolder": "/workspace"`と指定することでワークスペースを変更できる。

### workspaceMount

dockerコマンドの-v、docker-composeのvolumesに相当。

### mounts

-v引数に相当。volumeやworkspace外のディレクトリをマウントする際に使用する。

**source**はローカルのフォルダを指定。**target**はコンテナ内のフォルダを指定。typeはbindだとホストとコンテナ内のフォルダを同期する。volumeだと新規docker volumeを作成する。volumeの場合、sourceはvolumeの名前になる。

“”で括った範囲でカンマ（,）の前後で**スペースを入れてはいけない**。エラーとなる。

```bash
"source=${localEnv:HOME}/.aws,target=/root/.aws,type=bind,consistency=cached",
"source=node_modules,target=${containerWorkspaceFolder}/node_modules,type=volume"
```

### appPort

- ホストマシンからアクセスできるコンテナのポート番号を指定する。
- docker runするときの-pオプションで9000:9000と指定するのと同じ。

#### containerEnv

コンテナ内の環境変数を指定する。

### overrideCommand

`true` or `false`で指定。devcontainerはdockerのCMDコマンドをデフォルトで上書きする。上書きされたくない場合はfalseを指定する。

### initializedCommand

コンテナーの作成中およびその後の起動時を含む、初期化中に**ホストマシン**上で実行される

initializedCommand→onCreateCommand→updateContentComannd→postCreateCommand→postStartCommand→postAttachCommandと続く

### onCreateCommand

initializedCommandの後に実行。

- イメージをビルドした後にコンテナ内で実行されるコマンドを書くことができる
- dockerのCMDコマンドがこのコマンドで上書きされるわけではない（それがやりたいなら、overrideCommand: Trueで無効化）
- gitのインストールなど、VSCodeが開く前に実行したいコマンドを記述。postCreateCommand以降でgitやopenssh-clientをインストールすると、gitの設定がホストから引き継がれず、Gitが使用できない。リロードすれば使用できる。

### updateCreateCommand

onCreateCommandの後に実行。

Codespaceの設定。ローカルでは使用しなくて良い

### postCreateCommand

updateCreateCommandの後に実行。

VSCodeの操作が可能になる。gitのインストールなど、VSCodeが開く前に実行したいコマンドはこれよりも前に記述すべき。

### postStartCommand

postCreateCommandの後に実行。
VSCodeの操作が可能になる。gitのインストールなど、VSCodeが開く前に実行したいコマンドはこれよりも前に記述すべき。

前回開いた時のプロセスが止まっていない場合、ポートを占拠し続けて、起動するサーバーが起動できない。

### postAttachCommand

postStartCommandの後に実行。

コンテナにAttachするたびに実行されるコマンド。すでに起動されているコンテナにVScodeのコマンドパレット「実行中のコンテナにアタッチ」を実行した場合、唯一実行されるコマンド

前回開いた時のプロセスが止まっていない場合、ポートを占拠し続けて、起動するサーバーが起動できない

### shotdownAction

none, stopContainerが指定可能。defaultではstopContainer。

 VSCodeを閉じた時にコンテナの扱いを定義する。stopContainerはVSCodeがコンテナから接続を終了した際に、コンテナを停止する。コンテナの削除はされない。

stopContainerを指定をした場合の挙動は以下の通り。

VScode左下の”><”の「**フォルダーをローカルから開く**」「**リモート接続を終了する**」を選択すると停止する。またvscodeを❌で閉じても停止する。

VSCode右下の「><」から起動
![Screenshot 2024-06-03 at 15.20.16.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/679785/8de70c8d-5316-422e-c38d-18ad0e2b674f.png)


_**コンテナ内で起動した永続プロセス（サーバー起動など）を停止しない場合、コンテナは停止しない**_

### remoteUser

**コンテナイメージ内に存在するユーザー名を指定すること**で、コンテナを開いたときに使用されるユーザーを指定できる。Dockerfileでユーザーの追加記述が必要。



### extensions

コンテナ側のVS Codeにインストールされる拡張機能を指定できる。

ただし、開発メンバーによっては、どんな拡張機能を導入しているかまばらなことがあるので、基礎的なものだけを記述することを推奨する。

または、VScodeの設定で、**Containers: Default Extensions If Installed Locally**からコンテナをVSCodeで開くたびに導入する拡張機能を指定しても良い。

### 変数
devcontainer.json内で使用できる変数を紹介する

#### ${localWorkspaceFolder}

**ホストマシン**のワークスペースディレクトリを表す。.devcontainerが存在するフォルダ。

#### ${containerWorkspaceFolder}

**コンテナ内**のワークスペースディレクトリを表す

#### ${localEnv:VARIABLE_NAME}

ホストマシンの変数を指定できる。

例：${localEnv:HOME} 

→ホストマシンの$HOME変数を表している。

#### ${containerEnv:VARIABLE_NAME}

コンテナの変数を指定できる

例：${containerEnv:HOME}

→コンテナの$HOME変数を表している。

## devcontainer.json例

```json
{
  "name": "Rag LLM"　//VScodeの上部の枠に表示される名前
  ,"build" : {
    "dockerfile": "../Dockerfile",
    "target": "development",
    "context": "."
  } // ビルドするDockerファイルを指定
  ,"runArgs": [
    "--name=rag-llm",
    "--platform=linux/arm64"
  ] // devcontainer.jsonでは定義されていない引数を実装
  ,"workspaceFolder": "/usr/src/app"
  ,"workspaceMount": "source=${localWorkspaceFolder},target=${containerWorkspaceFolder},type=bind,consistency=cached" // -v $(pwd):$HOME のようなホストPCとコンテナ内のファイルをマウントする引数
  ,"mounts":[
	  "source=${localEnv:HOME}/.aws,target=/root/.aws,type=bind,consistency=cached",//AWSの認証情報をリモートにマウント
  ] // -v引数
  ,"appPort": ["15999:8080", "5679:5679"] // -p引数
  ,"containerEnv": {
    "PYTHON_ENV": "development"
  }, // コンテナ内の環境変数を定義
  ,"onCreateCommand": "apt install -y curl git openssh-client procps && poetry config virtualenvs.create false && poetry install --only dev --no-root" // gitやopenssh-client, 開発パッケージをインストール
  ,"postAttachCommand": "kill $(ps aux | grep 'uvicorn' | grep -v 'grep' | awk '{print $2}') || true && sleep 1 && DEBUG=1 python3 -m debugpy --listen 0.0.0.0:5679 -m uvicorn  app:app --host '0.0.0.0' --port 8080 --reload" // サーバーが存在したらプロセスキルして、新しいデバッグ用サーバーを設置
  ,"overrideCommand": true // DockerfileのCMDを無効化
  ,"shutdownAction": "stopContainer"
  ,"customizations": {
		"vscode": { 
			"extensions": [ // 拡張機能を定義
        "ms-python.python",
        "ms-python.autopep8",
        "ms-python.debugpy"
      ]
		}
		,"settings": { // VScodeの設定を記載
			"files.autoSave": "afterDelay" // ファイルのオートセーブ
			,"files.watcherExclude": { // ファイルの監視除外
				"**/.git/objects/**": true,
				"**/.git/subtree-cache/**": true
			}
		}
	}
}
```

## docker-compose.ymlを使用する場合

docker-compose.ymlファイルを指定する

`["../../docker-compose.yml"]`のようにプロジェクトルート外から参照することもできる。大きいプロジェクトで、機能やレポジトリーがそれぞれ分かれており、それをまとめるためのdocker-compose.ymlがある場合に便利

```json
{
  "name": "Dev container",
  "dockerComposeFile": "../docker-compose.yml",
  "service": "app", // 起動するアプリ
}
```

詳しくは公式リファレンスへ

https://containers.dev/implementors/json_reference/

# Tips

## devcontainer.jsonで指定できる〇〇Commandの挙動

VSCodedeのコマンドパレットから開いた時によって、〇〇コマンドの挙動が変わるので、それを元に解説する。
![Screenshot 2024-06-03 at 14.32.36.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/679785/1b01d58d-705a-4f9f-52bb-bae9544b31be.png)
「cmd + shift + p」で起動

### 「コンテナーでリビルドして再度開く」の挙動

initializedCommand→
onCreateCommand→
updateCreateCommand→
postCreateCommand→
postCreateCommand→
postStartCommand→
postAttachCommand

と順に起動していく

### 「コンテナーで再度開く」の挙動


```mermaid
graph TD
    A[Remote Containers 実行] --> B{コンテナが存在する}
    B -->|no| C[コンテナの作成および起動<br>ディレクトリやボリュームのマウント]
    B -->|yes| D[既存のコンテナに接続]
    
    C --> |コンテナ作成時の1回のみ実行| E[onCreateCommand]
    E --> L[updateContentCommand]
    L --> F[dotfilesのインストールや<br>.gitconfigファイルのコピー]
    
    F --> G[VS Code Server 起動]
    G --> M[postCreateCommand]
    M --> N{ }
    N -->|"同時並行で実施"| H[postStartCommand]
    H --> I[postAttachCommand]
    
    N -->|"同時並行で実施"| J[拡張機能インストール]
    I --> K[セッション接続、<br>VS Codeウィンドウの描画]
    
    style E fill:#87CEFA,stroke:#333,stroke-width:2px
    style H fill:#87CEFA,stroke:#333,stroke-width:2px
    style I fill:#87CEFA,stroke:#333,stroke-width:2px
    style L fill:#87CEFA,stroke:#333,stroke-width:2px
    style M fill:#87CEFA,stroke:#333,stroke-width:2px
    
    D --> N
    J --> K

  

```

**「実行中のコンテナにアタッチ」の挙動**

postAttachCommandのみ実行される

## ホストの.gitconfigが自動でコピーされる

何もしなくてもgitconfigのuser.nameやemailがコピーされる

```json
git config --global user.name "Your Name"
git config --global user.email "your.email@address"
```

## opensshがインストールしてあれば、Githubにpushできる

dockerコンテナ内にopnesshとgitをインストールすることで、ホストのssh-agent経由でGithubの設定を使用できる

```docker
RUN apk add --no-cache git openssh
```

ubuntu

```docker
RUN apt install add git openssh
```

ホストで以下のような設定ファイルが~/.ssh/configにあればOK

```
Host github.com
  HostName github.com
  AddKeysToAgent yes
  useKeychain yes
  IdentityFile ~/.ssh/id_rsa
  User git
```

ssh-agentにキーが追加されているか確認

```bash
ssh-add -l
```

ssh-agentにキーを追加していなければ、追加する。id_rsaは適宜変更。

macの場合はPC起動時に自動でこのコマンドが実行されるが、linuxとwindowsは.bashrcなどに追記が必要

```bash
ssh-add --apple-use-keychain ~/.ssh/id_rsa
```

#### 余談
ssh-agentとは、常にユーザーと一緒に行動して、必要な時に「はいっ！」と秘密鍵を渡してくる存在

クラウド上のサーバーを立ててそこからGithubに接続したい時、サーバーにGithubに入れる秘密鍵を置いておくのは危険なので、ssh-agentを使用する。のような使い方ができる。

## opensshをdockerファイル内でインストールせずにdevcontainer内でインストールする

onCreateCommandでインストールすることで、コンテナ内でインストールした時と同じ挙動になる（ssh-agentでホストの設定を受け継いでくれる）

```
  "onCreateCommand": "apk add curl git openssh && npm i && npm i -D",
```

## typescirptでのdevcontainerベストプラクティス

- workspaceMountでフォルダーを指定

- mountsでnode_modulesをボリューム化

- onCreateCommandでボリュームにnodeのパッケージをインストール。ボリュームマウントした場合、Docker内のnode_modulesが上書きされているため。

- overrideCommand: true, postAttachCommandで開発用サーバーを起動。起動したサーバーはコマンドから停止可能。

```
{
  "name": "Rag LLM",
  "build" : {
    "dockerfile": "../Dockerfile"
  },
  "runArgs": [
    "--name=rag-llm"
  ],
  "workspaceFolder": "/usr/src/app",
  "workspaceMount": "source=${localWorkspaceFolder},target=${containerWorkspaceFolder},type=bind,consistency=cached",
  "mounts": [
    "source=rag-llm-node_modules,target=${containerWorkspaceFolder}/node_modules,type=volume"
  ],
  "containerEnv": {
    "NODE_ENV": "local"
  },
  "appPort": ["15999:8080"],
  "onCreateCommand": "apk add curl git openssh && npm i && npm i -D",
  "postAttachCommand": "npm run dev",
  "overrideCommand": true
}
```

# Reference

devcontainerの公式リファレンス

https://code.visualstudio.com/docs/devcontainers/containers

devcontainerをわかりやすく解説した記事

https://zenn.dev/arkbig/articles/devcontainer_8d4f6fd4ade4efa79e0277fef1ad06470ae68

https://zenn.dev/nayushi/articles/5d577c93e03a9b

ssh-agentの解説

https://code.visualstudio.com/remote/advancedcontainers/sharing-git-credentials

https://zenn.dev/naoki_mochizuki/articles/ce381be617cd312ffe7f