Dockerコンテナ上で実行されているpythonプログラムでもステップ実行(リモートデバッグ)できれば開発効率が向上します。
Visual Studio Code(VSCode)と「ptvsd」というMicrosoftが開発するリモートデバッグ用ライブラリを組み合わせるとそれが可能になるので、その環境を構築してみました。
私の環境は以下の通りです。
構成 | 環境 | バージョン |
---|---|---|
ローカル側 (DockerホストOS) |
MacOS | 10.13.2 |
Docker for Mac | 17.12.0-ce-mac49 | |
VSCode | 1.20.1 | |
VSCodeの拡張機能「Python」 | 2018.1.0 | |
リモート側 (DockerゲストOS) |
AlpineOS | 3.7.0 |
Python | 3.6.4 | |
ptvsd | 3.0.0 |
Docker for Macなどで、Dockerコンテナをローカルマシンで起動できることを前提として話を進めます。
DockerのホストOSがWindowsでも、同様の流れでリモートデバッグが可能です。
目次
「VSCode」と「拡張機能 Python」をインストール
前回の記事を参考に、以下をホストOSにインストールします。
- VSCode
- VSCodeの拡張機能「Python」
サンプルプログラムを作成する
概要(ディレクトリ構成)
ローカル側に以下のディレクトリ構成でファイルを作成し、VSCodeでこのディレクトリを開きます。
/private/tmp/app/
├── Dockerfile
└── sample.py
後述するコンテナ起動時に、この構成をリモート側にそのままマウントします。
/opt/app/
├── Dockerfile ## リモート側では使わない
└── sample.py
リモートデバッグにおけるローカル側とリモート側のpyファイルの役割は、以下の通りです。
ファイル | 役割 |
---|---|
ローカル側のpyファイル | リモート側で実行されるプログラムをデバッグするための情報(何行目でどの変数を使って何をしているか等) |
リモート側のpyファイル | 書かれたプログラムを実行する |
今回はマウントする方針ですが、sample.py
をDockerfileのCOPYコマンドなどでコピーしてもリモートデバッグは可能です。
同一のディレクトリ構成で、同一のプログラムファイルがローカル側とリモート側の双方に存在することが重要です。
Dockerfile
以下の環境のコンテナを起動するDockerfile
を作成します。
- ゲストOSは軽量な「Alpine Linux」
- プログラムを実行する「python」
- リモートデバッグを可能にするライブラリ「ptvsd」
- コンテナ起動時に後述する
sample.py
をpythonで実行
FROM python:3.6.4-alpine3.7
RUN pip install ptvsd==3.0.0
RUN mkdir -p /opt/app/
CMD ["python", "/opt/app/sample.py"]
ptvsdのバージョンは3.0.0である必要があります。
これ以降のバージョンはまだサポートされておらず、正常に動きません。詳細はこちら。
sample.py
例として、以下をサンプルプログラムとして使用します。
import ptvsd
ptvsd.enable_attach("my_secret", address = ('0.0.0.0', 3000))
ptvsd.wait_for_attach()
ptvsd.break_into_debugger()
file = open('/etc/os-release')
content = file.read()
print(content)
file.close()
これがやっていることをプログラム中のコメントに書こうとしましたが、長くなるので以下に記します。
1〜4行目
ライブラリ「ptvsd」を使用して、リモートデバッグの準備をしています。
2行目で、pythonプログラムが外部(ポート番号3000で任意のIPアドレス)からアタッチ(リモートデバッグの開始)されるのを許可しています。
3〜4行目で、アタッチされるのを待機して、アタッチされたら次の行(6行目)で強制的にブレーク(処理を停止)させています。
wait_for_attach() と break_into_debugger() について
アタッチの待機が不要の場合、両方とも削除可能です。今回はリモートデバッグでブレークさせる説明のため採用しています。
wait_for_attach()のみだと、その直後の行にブレークポイントを設置してもブレークしません(ブレークできるようになるまで少し時間がかかる)。この2つのメソッドが必要な理由は、こちらのissueを参照ください。
6行目以降
ここから実際に動かしたいプログラムを書きます。やっていることは、ゲストOSの/etc/os-release
の中身(LinuxOSのバージョン情報)を表示しているだけです。
以下のコマンド(AlpineOS内でcat /etc/os-release
)と同一の内容を表示します。
$ docker run --rm python:3.6.4-alpine3.7 cat /etc/os-release
NAME="Alpine Linux"
ID=alpine
VERSION_ID=3.7.0
PRETTY_NAME="Alpine Linux v3.7"
HOME_URL="http://alpinelinux.org"
BUG_REPORT_URL="http://bugs.alpinelinux.org"
$
構成(launch.json)を修正する
VSCodeでローカル側とリモート側にあるpyファイルのディレクトリ位置をそれぞれ設定します。
どのファイルとどのファイルが同一のものであるか、リモートデバッグするために必要な情報です。
launch.jsonを表示する方法
VSCodeでsample.py
を表示している状態にして、メニューバーから[デバッグ] > [構成を開く] > [Python]を選択します。
開いているディレクトリ配下に.vscode/launch.json
が自動生成され、VSCodeに表示されます。
launch.jsonの修正箇所
以下のように赤文字の部分を修正すればOKです。(上記画像の下部に${workspaceFolder}
とデフォルトで書かれている2箇所)
“name”: “Python: Attach”
がある構成内の
- “localRoot”: “ローカル側のpyファイルがあるディレクトリ”,
- “remoteRoot”: “リモート側のpyファイルがあるディレクトリ”,
{
// IntelliSense を使用して利用可能な属性を学べます。
// 既存の属性の説明をホバーして表示します。
// 詳細情報は次を確認してください: https://go.microsoft.com/fwlink/?linkid=830387
"version": "0.2.0",
"configurations": [
{
"name": "Python",
...省略...
},
{
"name": "Python: Attach",
"type": "python",
"request": "attach",
"localRoot": "/private/tmp/app/",
"remoteRoot": "/opt/app/",
"port": 3000,
"secret": "my_secret",
"host": "localhost"
},
{
...省略...
コンテナにlocalhost
以外でアクセスする場合は、"host"
にコンテナのIPアドレス(192.168.x.x
など)やドメイン(xxx.local
など)を設定します。
Dockerコンテナを起動する
Docker上でpythonプログラムを実行させるために、コンテナを起動します。
Dockerfileからイメージをビルド
## 「sample_image」という名前でイメージをビルド
$ docker build /private/tmp/app/ --tag sample_image
## ビルドしたイメージを確認
$ docker images
REPOSITORY TAG IMAGE ID CREATED SIZE
sample_image latest 0a523d69746b 23 seconds ago 89.2MB
python 3.6.4-alpine3.7 4b00a94b6f26 6 weeks ago 83.4MB
$
ビルドしたイメージからコンテナを起動
オプションを使って、以下の設定でコンテナを起動する。
- ホストOSの
/private/tmp/app/
を、ゲストOSの/opt/app/
にマウント - ポート番号3000を公開
## 「sample_container」という名前でコンテナを起動
$ docker run -d -v /private/tmp/app/:/opt/app/ -p 3000:3000 --name sample_container sample_image
da5b798c88be252888b73b1f9d8dd2568b3b69c66989cc8ead8ab4ea02edd590
$
## コンテナの起動確認
$ docker ps -a
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
da5b798c88be sample_image "python /opt/app/sam…" 1 second ago Up 1 second 0.0.0.0:3000->3000/tcp sample_container
$
これで前述したsample.py
がコンテナ内で実行され、ptvsd.wait_for_attach()
の行でアタッチ待機の状態になりました。
Dockerコンテナ上のプログラムにリモートデバッグする
初回のリモートデバッグ
VSCodeで以下の操作をします。
- F1を押してコマンドパレットを表示
- 初期値で入力されている「>」を削除
- 「debug python」と入力
- 表示される「Python: Attach」を選択
ここまでの設定がうまく出来ているとリモート側でアタッチ待機しているプログラムに接続され、リモートプログラムの動作をVSCodeで操作可能になります。
そして、以下のようにptvsd.break_into_debugger()
の次の行でブレークされます。
9行目まで処理を進めると、変数content
にマウスカーソルを重ねるとその中身が見れたり、print文でAlpineOSのバージョン情報がデバッグコンソールに出力されます。(下記画像は冒頭のものと同一です。)
F5を押してsample.py
の実行を完了させると、コンテナのSTATUSがExited
になって終了します。
$ docker ps -a
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
da5b798c88be sample_image "python /opt/app/sam…" 11 minutes ago Exited (0) 3 minutes ago sample_container
$
2回目以降のリモートデバッグ
以下の操作を繰り返してプログラムを開発していくことができます。
- プログラム修正
- コンテナ起動(=プログラム実行)
- リモートデバッグ
プログラムを修正する
今度はファイルの中身の1行目だけを読み出すように、sample.py
の7行目をfile.read()
からfile.readline()
に修正してみました。
再度、コンテナを起動する
プログラムを修正後、以下のコマンドでコンテナを再度起動して、リモートプログラムを実行させます。
## コンテナは既に作成されているので、もう一度起動
$ docker start sample_container
sample_container
$
## 前述した"docker run"した直後と同じ状態になる
$ docker ps -a
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
da5b798c88be sample_image "python /opt/app/sam…" 13 minutes ago Up 1 second 0.0.0.0:3000->3000/tcp sample_container
$
再度、リモートデバッグする
今度はF5を押して、リモートプログラムにアタッチします。
以下のようにVSCodeの左下が、「Python: Attach(app)」と選択されている状態でF5を押すと、前述したコマンドパレットで「Python: Attach」を選択したのと同じ操作になります。
修正した通り、/etc/os-release
の1行目だけが出力されるようになりました。
このような流れで、VSCodeを使ってDocker上で実行されるプログラムを修正しながら、リモートデバッグが可能です。
まとめ
ローカルマシンで動くDockerコンテナ上で実行されているpythonプログラムを、VSCodeでリモートデバッグする手順をまとめました。
今まではリモートで動くpythonプログラムの変数の中身を確認する方法はログ出力くらいしか無いと思っていましたが、これで開発がかなり効率化できます。
VSCode、なかなか使えそうです。