$30 off During Our Annual Pro Sale. View Details »

Yet Another Isolation - Debian Packageと紐づく環境分離

Takumi Sueda
September 16, 2019

Yet Another Isolation - Debian Packageと紐づく環境分離

@PyCon JP 2019 A+B会議室

Python環境の分離ノウハウは数あれど、多くのPythonプロジェクトを1台のマシンに詰め込み、統一管理するノウハウはほぼありません。今回は、それを実現した手法と道のりを紹介します。

Takumi Sueda

September 16, 2019
Tweet

More Decks by Takumi Sueda

Other Decks in Technology

Transcript

  1. Yet Another Isolation
    Debian Packageと紐づく環境分離
    GROOVE X 株式会社 末田 卓巳

    View Slide

  2. • Python歴 5年, Go歴 3年
    • 社内の主な担当は Linux Kernel, FPGA
    • WEB+DB Press Vol. 104

    特集 「イマドキPython入門」 第1, 2章 執筆
    主なPythonの使いみち

    • Pythonの新機能で遊ぶ
    • 便利道具を作る・時々PyPIに公開する
    例: FPGAにRLEで焼き込むアセットの圧縮率最適化
    例: Googleのロケーション履歴から出退勤時間を算出
    自己紹介
    末田 卓巳
    SUEDA Takumi
    @puhitaku

    View Slide

  3. 2015年末に創業して以降、数回の調達を経て

    家族型ロボット「LOVOT(ラボット)」を開発中。
    現在は出荷に向けて準備を進めています。
    弊社紹介

    View Slide

  4. Y
    弊社紹介

    View Slide

  5. 弊社紹介 Y

    View Slide

  6. LOVOTのスタック
    Intel x86
    kernel
    rootfs
    Linux

    (Debian 9)
    ARM AArch64
    kernel
    rootfs
    Linux

    (Debian 9)
    Xilinx FPGA
    Y

    View Slide

  7. TOC
    • 開発の背景
    • LOVOTのソフトウェア管理
    • 類似技術の検討
    • 開発目標
    • 実現手法 / CLI設計
    • 実装
    • venv をラップして env を作る
    • 実行ファイルをルックアップする
    • env 内の実行ファイルをいい感じに走らせる
    • Shebangから呼び出すには
    • APTとsystemd
    • 現状の問題点

    View Slide

  8. 開発の背景

    View Slide

  9. LOVOTのソフトウェア管理
    Debian 9 (Stretch)
    Daemon D (Python)
    Daemon C (Python)
    CPython etc.
    Daemon B (C++)
    Daemon A (Go) シングルバイナリ, 外部依存なし
    libfoo.so.3
    Pythonパッケージ
    Pythonパッケージ
    Python には Python パッケージの依存関係が独自に存在するため

    それらも何らかの方法でインストールしなければならない

    → Python パッケージも APT でインストールすれば良いのでは?
    ★ LOVOT にデプロイするソフトウェア (daemon) はすべて

    Debian パッケージに格納して APT でインストールしている
    libbar.so.1

    View Slide

  10. APTの依存とPythonの依存
    ※ bleson = Bluetooth LE を HCI のレイヤーで操作できるパッケージ
    ★ 依存する Python パッケージの必要なバージョンを

    的確に APT でインストールできるとは限らない

    View Slide

  11. APTの依存とPythonの依存
    Daemon Y の依存パッケージ
    Daemon X の依存パッケージ
    パッケージ A
    3.9.39
    パッケージ B
    0.1.1
    パッケージ C
    19.09
    パッケージ D
    4.2.8
    パッケージ B
    0.2.13
    パッケージ C
    18.12
    Root Filesystem /usr/share/python3.x/dist-packages/
    パッケージ A
    3.9.39
    パッケージ B
    ??????
    パッケージ C
    ??????
    パッケージ D
    4.2.8
    ★ つまり、依存パッケージ同士は グローバルでなく 隔離された

    env (Python 環境) に住まわせなくてはならない
    ★ APT は Python パッケージを全てグローバル環境に置くため

    依存が被った場合 パッケージ間の衝突が発生する

    View Slide

  12. APTの依存とPythonの依存
    それなら… venv を使えばいいんじゃね?
    ★ LOVOT にデプロイするソフトウェア (daemon) はすべて

    Debian パッケージに格納して APT でインストールしている
    ★ 依存する Python パッケージの必要なバージョンをすべて的確に

    APT でインストールできるとは限らない
    ★ APT は Python パッケージを全てグローバル環境に置くため

    依存が被った場合 パッケージ間の衝突が発生する
    ★ 依存パッケージ同士は グローバルでなく 隔離された

    env に住まわせなくてはならない

    View Slide


  13. APTから直接venvを呼ぶ?
    APTでは Debian パッケージのインストール・アンインストールをフックして

    自由にスクリプトを実行できる → apt install 時に env を作れば良さそう
    #!/bin/sh -e
    python3 -m venv /usr/share/python-envs/miku
    source /usr/share/python-envs/miku/bin/activate
    pip install -r /usr/share/miku/requirements.txt
    systemctl enable miku
    postinst deb パッケージがインストールされた後に実行する
    #!/bin/sh -e

    systemctl disable --now miku
    rm -rf /usr/share/python-envs/miku
    prerm インストールされたファイルが削除される前に実行する
    Mikuというdaemon専用のenvを作成・削除する

    View Slide

  14. APTから直接venvを呼ぶ?
    確かに問題は解決するけれど…
    Debian パッケージごとに env が分離できる

    システムの 「どこに」 「いくつ」 「誰が作った」 env があるか

    わからない

    全員が思ったとおりにenvを作って消してくれる保証がない

    例: 場所や --system-site-packages など

    いちいち activate するのが面倒

    Debian Repository にない Python パッケージも導入可能

    View Slide

  15. APTから直接pyenvを呼ぶ?
    env を集中管理したいなら…

    pyenv を使えばいいんじゃね?

    View Slide

  16. APTから直接pyenvを呼ぶ?
    pyenvでも確かに問題は解決するけれど…
    Debian パッケージごとに Python 環境が分離できる

    virtualenvとの連携機能により 決まった箇所にenvを置き

    集中管理できる

    pyenv に不慣れな人は極めて事故りやすい

    Shell実装で難解

    いちいち activate するのが面倒

    Debian Repository にない Python パッケージも導入可能

    View Slide



  17. View Slide


  18. APT, venv, pyenv その他を調べたものの
    欲しいものは手に入らなさそうだったので
    自分で欲しいツールを作ることにした

    View Slide

  19. 開発目標
    こういうのが欲しい!!
    Debian パッケージごとに Python 環境が分離できる

    決まった箇所にenvを置き集中管理できる

    env と実行したいファイルを明示的に指定して一発で実行できる

    (activateが不要)

    Shebangからでも一発で呼べる

    APT や systemd といい感じに連携可能

    View Slide

  20. 作りました
    github.com/groove-x/gxenv
    pypi.org/project/gxenv

    View Slide

  21. 実現手法と

    CLIをどうするか考えてみる

    View Slide

  22. 実現したいこと
    実現手法とCLIを考える
    Debian パッケージごとに Python 環境が分離できる

    $ gxenv create foo
    $ gxenv list
    /var/lib/python3/envs/foo
    $ gxenv purge foo
    手法
    • 設定ファイルで指定されたパスに env を作る
    • create で作り、list で一覧表示し、purge で消す
    決まった箇所にenvを置き集中管理できる

    View Slide

  23. 実現したいこと
    実現手法とCLIを考える
    $ gxenv run foo pip install requests
    Collecting requests
    ...
    手法
    • run サブコマンドを用意し、 env 名・コマンド名・コマンド引数を

    もとにパスを解決して実行 (プロセスの切り替え手法は後述)
    env と実行したいファイルを明示的に指定して一発で実行できる

    Shebangからでも一発で呼べる

    #!/usr/bin/gxenv run foo python3
    print('Hello World!')

    View Slide

  24. 実現したいこと
    実現手法とCLIを考える
    手法
    • APT の postinst, prerm から create/purge を呼ぶ
    • systemd の ExecStart から run を呼ぶ
    APT や systemd といい感じに連携可能

    View Slide

  25. 実現手法とCLIを考える
    #!/bin/sh
    gxenv purge miku
    #!/bin/sh
    gxenv create miku
    gxenv run miku pip install /usr/share/miku/requirements.txt
    ...
    [Service]
    ExecStart = /usr/bin/gxenv run miku hatsune_miku --arg1 --arg2
    postinst
    prerm
    miku.service

    View Slide

  26. CLI
    env を作る ・ 消す ・ 一覧表示する
    結果、サブコマンドはこんな感じで行くことに
    gxenv create, purge, list
    env の中の実行ファイルのパスを表示
    gxenv which
    $ gxenv create miku
    $ gxenv list
    /var/lib/python3/envs/miku
    $ gxenv purge miku
    $ gxenv which miku python3
    /var/lib/python3/envs/miku/bin/python3

    View Slide

  27. CLI
    env の中の実行ファイルを実行
    結果、サブコマンドはこんな感じで行くことに
    gxenv run
    $ gxenv run miku python -m site
    sys.path = [
    '/home/user',
    '/usr/lib/python37.zip',
    '/usr/lib/python3.7',
    '/usr/lib/python3.7/plat-x86_64-linux-gnu',
    '/usr/lib/python3.7/lib-dynload',
    '/var/lib/python3/envs/miku/lib/python3.7/site-packages',
    ]
    ...
    #!/usr/bin/gxenv run miku python3
    print('Hello World!')

    View Slide

  28. 実装!
    venv をラップして env を作る

    View Slide

  29. おさらい
    • PEP 405で提案され、Python 3.3 より導入された 「仮想環境を

    作成する軽量なメカニズム」
    • PyPA がホストしていた virtualenv を改良し標準ライブラリに導入
    • 標準ライブラリにあるので Python プログラムより呼び出し可能

    (Batteries Included!!)
    venv とは?

    View Slide

  30. venv の API
    • path: envを作るディレクトリ
    • system_site_packages: グローバルなパッケージを参照するかどうか
    • clear: 既にディレクトリがあった場合削除するかどうか
    • symlinks: envの中に置くインタプリタ等をsymlinkにするかどうか
    • with_pip: pipをインストールするかどうか
    • prompt: (optional) activateした時の prompt を指定する
    venv.create(env_dir, system_site_packages=False,

    clear=False, symlinks=False, with_pip=False, prompt=None)
    venv には 動作をカスタマイズできる EnvBuilder class と

    簡易に呼び出して env を作るための関数がある
    モジュール関数

    View Slide

  31. gxenv で env を作る実装
    gxenv/__init__.py: fmt_env_path()
    p = pathlib.Path(const.env_base) / env_name
    ...
    return str(p)
    30
    33
    env の作成先を venv.create に渡すだけで env ができる!簡単!
    gxenv/__init__.py: cmd_create()
    path = fmt_env_path(env_name)
    ...
    venv.create(
    path, system_site_packages=False, clear=False, ↩
    symlinks=True, with_pip=True
    )
    134
    139
    140
    141
    ※1 const.env_base = envを置く場所のbase path
    ※2 env_name = envの名前

    View Slide

  32. 実装!
    実行ファイルをルックアップする

    View Slide

  33. 実行ファイルの見つけ方
    実行可能ファイルを見つけるのに手動での glob やフィルタは必要ない
    • cmd: 見つけたい実行ファイル名
    • mode: 見つけたいファイルのパーミッションマスク
    • path: 探すPATH, Noneだと os.environ['PATH'] か os.defpath
    shutil.which(cmd, mode=os.F_OK | os.X_OK, path=None)
    モジュール関数
    >>> import shutil
    >>> shutil.which('vim')
    '/usr/local/bin/vim'

    View Slide

  34. 実行ファイルの見つけ方
    env のパスを出して、そのディレクトリに対して which するだけ
    gxenv/__init__.py: which()
    env_dir = fmt_env_path(env_name, "bin")
    ...
    found = shutil.which(executable_name, path=env_dir)
    ...
    return found
    226
    232
    241
    gxenv/__init__.py: cmd_which()
    found = which(env_name, executable_name, ↩
    verbose=verbose)
    ...
    print(found)
    254
    260

    View Slide

  35. 実装!
    env 内の実行ファイルを

    いい感じに走らせる

    View Slide

  36. env 内の実行ファイルをいい感じに走らせる
    • 別のプログラムを実行するには subprocess を使うことが多い
    • subprocess は Python の子プロセスとして起動されるため

    プロセスツリーにPythonが鎮座する事になる
    • gxenv が目的のプログラムを起動した時点で仕事を終えているため邪魔
    >>> import subprocess
    >>> subprocess.run(['vim'])
    subprocess?
    tmux
    \_ /bin/bash
    \_ python
    \_ vim
    ps -xf -o cmd
    ← いらない

    View Slide

  37. ところで
    Shebang (#!) でよく使われる env コマンドは

    目的のコマンドを起動したあとプロセスツリーから消える
    $ /usr/bin/env vim
    tmux
    \_ /bin/bash
    \_ vim
    ps -xf -o cmd
    Shell
    ← Shell の直下で動いている!
    gxenv もこれになりたい…どうやって?

    View Slide

  38. env コマンドに学ぶ
    /usr/bin/env が別のプログラムにすり替わるからくりは

    システムコール execve(2) で実現している
    execveとは? from manpage execve(2)
    execve() executes the program pointed to by filename.
    ...
    execve() does not return on success, and the text, data, bss, and
    stack of the calling process are overwritten by that of the program
    loaded.
    execve() は filename で示されたプログラムを実行します。

    execve() が成功したときはreturnしません。呼び出したプロセスの text, data, bss
    (セクション), そしてスタックは、ロードされたプログラムのもので上書きされます。

    View Slide

  39. env コマンドに学ぶ
    coreutils/src/env.c: main()
    execvp (argv[optind], &argv[optind]);
    Source: github.com/coreutils/coreutils/blob/master/src/env.c
    943
    glibc/posix/execvp.c: execvp()
    return __execvpe (file, argv, __environ);
    Source: sourceware.org/git/?p=glibc.git;a=blob;f=posix/execvp.c
    26
    glibc/posix/execvpe.c: __execvpe_common()
    return __execve (file, argv, envp);
    Source: sourceware.org/git/?p=glibc.git;a=blob;f=posix/execvpe.c
    190
    ・・・
    syscallを呼んでいる
    execvp = 実行ファイルの絶対パスを出してからexecveするlibcの関数
    execveに至るまでのコールスタック

    View Slide

  40. env 内の実行ファイルをいい感じに走らせる
    Python から execve(2) を呼ぶのは超簡単!
    import os
    os.execv('/usr/local/bin/vim', ['vim'])
    実行すると、Python インタプリタは vim に差し替わる
    os.execve(path, args, env)
    path: 実行ファイルの絶対パス
    args: 渡す引数(長さは必ず1以上)
    env: 新しい環境変数のdict
    実行例
    API
    os.execv(path, args)
    path: 実行ファイルの絶対パス
    args: 渡す引数(長さは必ず1以上)
    (本来execveは新しい環境変数の組を与えるが、変更しない場合はexecvが使える)

    View Slide

  41. env 内の実行ファイルをいい感じに走らせる
    ここまでに出てきた 実行ファイルを見つける処理と

    execve で実行する処理を組み合わせると
    run サブコマンドが実装できる!

    View Slide

  42. 実装!
    Shebang から呼び出すには

    View Slide

  43. おさらい
    • #! で始まるテキストファイルの冒頭行のこと
    • 解釈方法は統一されていないが Unix-like OS なら直接実行できる

    See also: github.com/torvalds/linux/blob/master/fs/binfmt_script.c
    #!/usr/bin/env python3
    print('Hello World!')
    $ ./helloworld.py

    Hello World!
    Shebang で直接 env を指定できると activate 要らずで嬉しい!
    #!/usr/bin/gxenv run foo python3
    print('Hello World!')
    実行
    helloworld.py
    Shebang とは?

    View Slide

  44. Shebang から呼び出すには
    Shebang に書かれた env の Python を実行するには

    何をしてやる必要がある?
    #!/usr/bin/gxenv run foo python3
    print('Hello World!')

    View Slide

  45. 動作を観察する
    Shebang 経由で呼ばれた Python の sys.argv を観察してみる
    gxenv
    #!/usr/bin/env python3
    import sys
    print('\n'.join(sys.argv))
    run
    $ ./run
    gxenv
    run
    foo
    python3
    ./run
    実行
    "gxenv" "run" "foo" "python3" "./run" が渡ってきた
    #!/usr/bin/env -S gxenv run ↩
    foo python3
    Linux / macOS 互換には
    /usr/bin/env -S が有用

    View Slide

  46. gxenv での shebang 実装
    argparse に流し込んで gxenv に対する引数とそうでない引数を分ける
    "gxenv" "run" "foo" "python3" "./run"
    parsed
    parsed, unknown = parser.parse_known_args()
    env名 コマンド名
    gxenv
    unknown コマンド引数
    あとは 「foo」 という env にある 「python3」 を

    execve して 「./run」を渡す
    サブコマンド

    View Slide

  47. 実装!
    APT と systemd との

    インテグレーション
    gxenv を使ったアプリケーションの

    Debian パッケージ例

    View Slide

  48. インテグレーション例

    冒頭で出た 「APT の install/remove
    をフックして env を作る」 と同じ流れ
    Mikuというdaemon専用のenvを作成・削除する

    View Slide

  49. インテグレーション例
    #!/bin/sh -e
    gxenv create miku
    gxenv run miku pip install -r /path/to/requirements.txt
    systemctl enable miku
    postinst deb パッケージがインストールされた後に実行する
    #!/bin/sh -e

    systemctl disable --now miku
    gxenv purge miku
    prerm インストールされたファイルが削除される前に実行する

    View Slide

  50. インテグレーション例
    miku.service systemd の daemon (service) 定義
    [Unit]
    Description=HatsuneMiku
    [Service]
    ExecStart=/usr/bin/gxenv run miku miku
    Restart=always
    [Install]
    WantedBy=multi-user.target

    View Slide

  51. まとめ

    View Slide

  52. 開発目標
    こういうのが作れた!!
    Debian パッケージごとに Python 環境が分離できる

    決まった箇所にenvを置き集中管理できる

    env と実行したいファイルを明示的に指定して一発で実行できる

    (activateが不要)

    Shebangからでも一発で呼べる

    APT や systemd といい感じに連携可能

    View Slide

  53. 現状の問題点
    Numpy のようなヘビーな Python パッケージを複数の
    env でインストールすると容量を圧迫する

    View Slide

  54. gxenv を使う方法
    https://pypi.org/project/gxenv/
    PyPI にあります。
    注意!

    PyPI 版は /usr/bin/gxenv のような実行ファイルを置かないので

    python -m gxenv で CLI を呼ぶ必要があります

    質問があれば Twitter @puhitaku 等でお聞きください

    View Slide