VSCode に初めてふれてから Docker volume マウントして Python remote debug するまで

最近、少人数のチームで開発している。Docker を試しに導入した。いい線をいっている。だが、ちょっとかゆいところに手が届いていない。

ローカル環境では以下の２点を同時に実現したいと思った。

- コンテナが起動している状態で、すぐにコードの変更を Docker 環境に反映されること
- Docker 環境にたいしてリモートデバッギングできること

プロジェクトも遅延していて体調不良になっている人もいたのでフォローに入ったのだが、ちょっと調べてみても両方を実現している事例が見当たらなかった。

メモ程度に残しておく。

# 基本的な構成

ここはほとんど同僚ががんばってくれたところ。なんどもゼロからつくるのは時間がかかってしんどい。そこで以下のように base となるイメージと派生させるイメージの二つを作った。

- base: ほとんどダウンロードやインストールが重いものはこちらで作成。
- debug: base イメージを利用。コンテナでホストのディレクトリをマウント。
- server: base イメージを利用。ホストにあるコードをコンテナにコピー。

# コード変更の即時反映

以前パフォーマンスチューニングをしたときに、サーバで変更した修正をローカルPCに反映するのが煩わしかった。環境をそろえるためにローカル PC に Docker を入れたのに、コードの移動がローカル PC で発生するのはちょっと意味がわからなかった。

ぱっと思いつくのは、NFS マウントと同じことができればいいだろう、同じことを考える人はいるだろう、という見立てがあった。

調べてみると方法は２通りあった。Docker コマンドで volume と mount というオプションがあるらしかった。比較的情報が多かった volume を採用した。

```
docker run -v {ホストでコードがあるディレクトリ}:{コンテナでマウントしたいディレクトリ} {dockerイメージ} {コンテナで実行するコマンド}
```

# リモートデバッグ

これまで PyCharm 無料版を使っていたが、Docker と連携するには有料版が必要らしい。VSCode では無料でも対応可能と聞いていたので試してみることにした。

## VSCode の設定

### plugin の導入

わからないなりに以下のプラグインを入れてみた。
依存関係があるようなので、一緒に入ってしまったものがあるかも。

- MS-Python
- Python Extension Pack
- markdownlint
- MagicPython
- Jupyter
- Jinja
- Django Template
- Django Snippet
- Docker

### 設定の変更

launch.json というファイルに "Python: Attach" という項目があった。その中にある localRoot と remoteRoot を volume の設定に合わせて修正した。

以下は設定のイメージ。

```launch.json:
...
        {
            "name": "Python: Attach",
            "type": "python",
            "request": "attach",
            "localRoot": "${workspaceFolder}/app",
            "remoteRoot": "/opt/app",
            "port": 3000,
            "secret": "my_secret",
            "host": "localhost"
        },
...
```

## デバッグ用コードの作成

今回は flask で http サーバを作ることにしたので、それを再利用した。

以下は実装のイメージ。デバッグしないコンテナ用のコードが先にあった。

```server.py:
import flask
from flask import Flask, request

app = Flask(__name__)

@app.route('/post', methods=['POST'])
def post():
    ...

if __name__ == '__main__':
    app.run(host='0.0.0.0', port=80)
```

上記のファイルを再利用して、デバッグするコンテナ用のコードを作成した。

```debug.py
import ptvsd
from server import app

if __name__ == '__main__':
    print("waiting....")
    ptvsd.enable_attach("my_secret", address=('0.0.0.0', 3000))
    ptvsd.wait_for_attach()

    app.run(host='0.0.0.0', port=80)
```

要点は ptvsd というモジュールだった。`ptvsd.wait_for_attach()`でずっと待ち状態になる。

VSCode から Attach されると次の `app.run(hot='0.0.0.0', port=80)`が実行される。

# はまりポイント

ここまでは情報があったのだが、素直にコンテナで volume でマウントしたディレクトリのファイルを実行しようとするとファイルが見つからないらしく、実行されなかった。

必要最小限の構成を目指していたために組み込み系の linux のイメージを利用していて、状況把握に時間がかかった。半日ほどかかってしまったが、結局、debug.py を実行するスクリプトをつくり、sleep を入れてマウントされるまでの時間を稼いで回避した。

以下は Dockerfile、debug ファイルの実行スクリプト、debug 用 docker コンテナの起動スクリプトのイメージ。

```Dockerfile:
FROM base:latest

RUN mkdir -p /opt/app

ENV PYTHONPATH /opt/app

RUN pip --no-cache-dir install ptvsd==3.0.0

RUN mkdir -p /root/bin
COPY {ホストのデバッグ用スクリプトを配置したディレクトリ}/debug_server.sh /root/bin
RUN chmod 755 /root/bin/debug_server.sh

EXPOSE 80
EXPOSE 3000
```

ユーザ作成とかしてないのでやっつけ感が漂っていますな。。。

```debug_server.sh:
#!/bin/sh

sleep 3

if [ /opt/app/debug.py ]; then
  /usr/bin/python /opt/app/debug.py
fi
```

VSCode の Docker プラグインはいい感じによく使われるところだけ指定するようになっているように見えた。しかしながら細かいパラメータの指定が微妙だったのでスクリプトにした方が早いように思われた。

```launch_docker.sh:
#!/bin/bash

docker run --rm -it -d -v {workspceのディレクトリ}/app:/opt/app -p 3000:3000 -p 8080:80 debug:latest /root/bin/debug_server.sh
```

# 最後に

起動したコンテナに attach してから以下を確認した。

- ps コマンドを実行すると `python /opt/app/debug.py` が実行されること
- netstat コマンドではポート 3000 のみがリッスンされていること
- VSCode で attach してデバッガが実行されたこと
- curl コマンドでリクエストすると、関連したブレークポイントで停止したこと
- attach した後に netstat コマンドではポート 80 のみがリッスンされていること