VSCode x Remote Development x Pipenvで快適開発環境構築

# 概要
[Docker](https://www.docker.com/)と[Pipenv](https://pipenv.kennethreitz.org/en/latest/#)でPythonの実行環境を管理して、それを[VSCode](https://code.visualstudio.com/)の[Remote Development](https://code.visualstudio.com/docs/remote/remote-overview)で切り替える仕組みについて紹介します。[Facebookがこの機能のためにMicrosoftと提携したというニュース](https://gigazine.net/news/20191120-facebook-microsoft-partnering-remote-development/)が最近あるくらい今アツい機能です。これを導入することで得られる恩恵としては、

- Docker: Pipenvでは管理できない依存ライブラリ(LAPACKなど)や環境変数の設定なども管理可能
- Pipenv: 開発にのみ必要なpackage(autopep8など)を実行環境と分離して管理でき、かつ、pipよりも依存管理が容易
- VSCode: 上記2つで構築したdocker container上でシームレスに開発可能で、各種extensionが強力

などがあるので、これらを組み合わせて開発環境を構築するメリットは大きいと思います。イメージとしては以下のような形で構成します。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/234668/83c413da-c0ad-9ea6-3a4c-52b665e117c4.png)

また、VSCodeでPythonの開発をする上でオススメの拡張機能や設定も紹介しています！

# Version情報
- VSCode: 1.40.1
- Docker: 18.09.7
- docker-compose: 1.24.1

# 下準備
- VSCodeのinstall: [Downloadページ](https://code.visualstudio.com/Download)からサクッと
- Dockerのinstall: [Community版](https://docs.docker.com/install/linux/docker-ce/ubuntu/)をinstallして[non-rootで動かす設定](https://docs.docker.com/install/linux/linux-postinstall/)も

公式以外で詳しい説明はいろんな記事があると思うので、上記で不十分な方は各々調べてください。

# 環境構築のステップ
1. Dockerfileを作成
2. Pipfileを作成
3. docker-compose.yml に実行環境用のbuild方法を記載
4. Remote Developmentの拡張機能をinstall
5. docker-compose.extend.yml にRemote Development特有の設定を記載
6. .devcontainer.jsonに必要な設定を記述
7. VSCodeでコマンド打ってbuild!!

1~3はプロダクトをDockerやPipenvを利用して運用している方は既に作ってあると思いますので、4まで読み飛ばしてください。ちょっと道のりは長いですが、快適環境を目指して頑張りましょう！

# やり方解説
以下の構造で設定ファイルを記述していきます。それぞれのファイルの役割と作り方を順に解説していきます。{repo-name}はレポジトリ名、{tool-name}は開発ツールのroot directoryと読み替えてください。

```tree
{repo-name}
├── .devcontainer
│   ├── devcontainer.json
│   └── docker-compose.extend.yml
├── Dockerfile
├── Pipfile
├── docker-compose.yml
└── {tool-name}
```

## Dockerfileの作成
恥ずかしながらそこまでDockerに詳しいわけではないので、ツッコミどころは多々あると思いますが、例えば以下のような形で環境を作ります。

```Dockerfile:Dockerfile
# Python 3.8ならslim-busterがオススメらしい
# 参照: https://pythonspeed.com/articles/base-image-python-docker-images/
FROM python:3.8-slim-buster

# 環境変数とかを設定
ENV HOME /root
ENV TZ Asia/Tokyo
WORKDIR $HOME

# Pythonで利用するライブラリをいろいろinstall
# software-properties-commonとwgetは後段のclang 8のinstallに必要
RUN set -ex \
  && apt-get update \
  && apt-get install -y g++ git openssh-client wget libblas-dev liblapack-dev gnupg software-properties-common make

# clang 8のinstall
# numbaがllvmliteに依存しているので、clang 8系をまとめてinstall
RUN wget -O - https://apt.llvm.org/llvm-snapshot.gpg.key | apt-key add - \
  && add-apt-repository "deb http://apt.llvm.org/buster/ llvm-toolchain-buster-8 main" \
  && apt-get update \
  && apt-get -y install clang-8 lldb-8 lld-8 gfortran

# llvm-configへのpathを登録
ENV LLVM_CONFIG=/usr/lib/llvm-8/bin/llvm-config

# host key情報をknown_hostsに記載
# 参照: https://gist.github.com/gregdeane/56a7499fddac7251f01dcc9bb64e8486
RUN mkdir -p -m 0600 ~/.ssh \
  && ssh-keyscan github.com >> ~/.ssh/known_hosts

# Pipenv のinstall
RUN pip3 --no-cache-dir install pipenv

# 開発したいレポジトリ名でディレクトリを作っておく.
RUN mkdir {repo-name}
```

今回はGitHub上のprivate repositoryを開発することを前提としているので、Dockerfile内ではrepositoryのcloneや依存のinstallまでは行いません。[docker-compose側で--ssh defaultのサポートがまだ行われていない](https://github.com/docker/compose/pull/6865)ので、別の方法でinstallします。もし、public repositoryで開発する場合は、Dockerfileで`git clone`や`pipenv install --dev`も行ったほうが良いと思います。

## Pipfileの作成
実行環境に必要なライブラリを追加しつつ、開発環境で必要なツール群は`[dev-packages]`以下に記載しましょう。例えば、

```Pipfile:Pipfile
[[source]]
url = "https://pypi.org/simple"
verify_ssl = true
name = "pypi"

[packages]
Cython = "*"
joblib = "*"
numpy = "*"
scipy = "*"
scikit-learn = "*"
lightgbm = "*"

[dev-packages]
nose = "*"
coverage ="*"
flake8 = "*"
isort = "*"
Sphinx = "*"
sphinx-autodoc-typehints = "*"
autopep8 = "*"
setuptools = "*"
mypy = "*"

[requires]
python_version = "3.8"
```

こんな感じです。まずは実際に上記のDockerfileを元に `docker run` をしてみて、このPipfileが正しく動作するかを確認してみましょう。

## Remote Developmentの導入
[Remote DevelopmentのTurorial](https://code.visualstudio.com/docs/remote/remote-tutorials)を元にinstallします。これは以下のVSCodeのGUIから簡単にinstallすることができます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/234668/5aae96ad-6d76-3e53-5277-279f0cfe07d9.png)

簡単ですね。

## docker-compose.yml に動作環境用のbuild方法を記載
上述のDockerfileをbuildする`docker-compose.yml`を作成します。`build-arg`を指定する必要があるようなDockerfileの場合はここで設定してください。最もシンプルな例だと、

```yaml:docker-compose.yml
version: "3.7"
services:
  dev:
    build:
      context: .
      dockerfile: Dockerfile
```

のような形かと。

## docker-compose.extend.yml にRemote Development特有の設定を記載
ここからが本番ですね。Remote Developmentをするのに必要な設定のみで構成された`docker-compose.extend.yml`を作成します。次のセクションで作成する`.devcontainer.json`で２つのyamlファイルを渡すことでそれぞれの設定が順番に適用された設定でbuildが行われます。例えば、

```yaml:docker-compose.extend.yml
version: "3.7"
services:
  dev:
    volumes:
      - ~/.ssh/id_rsa:/root/.ssh/id_rsa:ro
    command: sleep infinity
```

のように作ります。

`volumes`の項目ではlocalの公開鍵をRemote Developmentで利用するDocker imageにmountしています。なので、自身のlocal machineの公開鍵をGitHubに登録しておく必要があります。詳しいやり方は[こちら](https://help.github.com/en/github/authenticating-to-github/connecting-to-github-with-ssh)を参照してください。繰り返しになりますが、public repositoryで開発する場合は認証がいらないので不要な項目です。

また、`command`は今回必須です。これがないと、Remote Developmentはbuildだけして死んでしまうので…

## .devcontainer.jsonに必要な設定を記述
ここまでくればもう一息です。上で作成したDockerfileのbuild方法などを指定するファイルです。以下に例と、それぞれのパラメータがもつ役割をコメントで記載していきます。

```json:.devcontainer.json
{
    # Workspaceの名前になる.
	"name": "{repo-name} DevEnv",

    # 設定ファイルのありかを指定する場合に使うルートディレクトリ.
	"context": ".",

    # docker-compose upで利用されるymlへのpathのリスト.順番厳守.
	"dockerComposeFile": [
		"../docker-compose.yml",
		"docker-compose.extend.yml"
	],

    # 開発環境用のbuild設定が記載されたdocker-compse.yml上のservice名.
	"service": "dev",
	"shutdownAction": "stopCompose",

    # VSCodeのProjectになるroot directory.
	"workspaceFolder": "/root/{repo-name}",

    # Dockerのbuildが完了した際に実行するコマンド.ここでrepositoryのcloneとinstallを行う.
	"postCreateCommand": "git clone -b {branch name} {GitHub url} . && pipenv install --dev",

    # Remote Developmentで立ち上げた新たなVSCodeに適用する設定.
	"settings": {
		"autoDocstring.docstringFormat": "sphinx",
		"autoDocstring.guessTypes": false,
		"editor.formatOnSave": true,
		"editor.suggestSelection": "first",
		"git.autofetch": true,
		"git.confirmSync": false,
		"kite.showWelcomeNotificationOnStartup": false,
		"python.formatting.autopep8Args": [
			"--max-line-length=200"
		],
		"python.linting.pylintEnabled": false,
		"python.linting.flake8Args": [
			"--max-line-length=200"
		],
		"python.linting.flake8Enabled": true,
		"python.linting.mypyEnabled": true,
		"python.jediEnabled": false,
		"terminal.integrated.inheritEnv": false,
		"vsintellicode.modify.editor.suggestSelection": "automaticallyOverrodeDefaultValue"
	},

    # Remote環境で採用したい拡張機能のIDを登録しておくと、環境構築時に自動でinstallしてくれる.
	"extensions": [
		"ms-python.python",
		"njpwerner.autodocstring",
		"ms-azuretools.vscode-docker",
		"visualstudioexptteam.vscodeintellicode"
	]
}
```

Extensionについて補足すると、

### njpwerner.autodocstring
Pythonで関数やクラスのdocstringを自動生成してくれるツール.[Sphinx](http://www.sphinx-doc.org/en/master/)でドキュメントのhtmlを自動で生成したいので、`"autoDocstring.docstringFormat": "sphinx"`でdocstringのstyleも設定しています。

### visualstudioexptteam.vscodeintellicode
Python向けに補完などを非常に強力にやってくれる超便利ツール.`"python.jediEnabled": false`が必須の設定になります。

### python.linting
Python3系でType Hintingを記載しないのは罪だと思っているので、`"python.linting.mypyEnabled": true`は必須で、あとは今時のモニタでmax-line-lengthがデフォルトなのは読みづらくて仕方がないので、`autopep8`と`flake8`のそれぞれで`"--max-line-length=200"`を指定しています。VSCodeのPythonのデフォルトのlinterは[pylint](https://www.pylint.org/)なのですが、ぼくが使った時にimport周りでやたらハマった経験があったので、[flake8](http://flake8.pycqa.org/en/latest/)を使っています。

という感じです。

## VSCodeでコマンド打ってbuild!!
(Ubuntuの場合)VSCode上でCtrl + Shift + pでcommand windowを開いて以下のように `Open Folder in Container...`を叩いて、

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/234668/acb62a54-7087-952e-57f6-eea207f186d7.png)

localのDockerfileがあるディレクトリを指定すれば、自動でbuildが始まってDocker Container上に構築された環境上で動くVSCodeが新たに立ち上がります。初回時はdockerのbuildやpipenv installに時間がかかるので辛抱強く待ちましょう。

最後にPythonのInterpreterがpipenvの環境を認識しないことがありますが、command windowから`reload window`を実行すれば直ります。

これで開発環境は完成です！お疲れ様でした！！