Upgrade to Pro — share decks privately, control downloads, hide ads and more …

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. • Python歴 5年, Go歴 3年 • 社内の主な担当は Linux Kernel, FPGA

    • WEB+DB Press Vol. 104
 特集 「イマドキPython入門」 第1, 2章 執筆 主なPythonの使いみち
 • Pythonの新機能で遊ぶ • 便利道具を作る・時々PyPIに公開する 例: FPGAにRLEで焼き込むアセットの圧縮率最適化 例: Googleのロケーション履歴から出退勤時間を算出 自己紹介 末田 卓巳 SUEDA Takumi @puhitaku
  2. TOC • 開発の背景 • LOVOTのソフトウェア管理 • 類似技術の検討 • 開発目標 •

    実現手法 / CLI設計 • 実装 • venv をラップして env を作る • 実行ファイルをルックアップする • env 内の実行ファイルをいい感じに走らせる • Shebangから呼び出すには • APTとsystemd • 現状の問題点
  3. 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
  4. APTの依存とPythonの依存 ※ bleson = Bluetooth LE を HCI のレイヤーで操作できるパッケージ ★

    依存する Python パッケージの必要なバージョンを
 的確に APT でインストールできるとは限らない
  5. 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 パッケージを全てグローバル環境に置くため
 依存が被った場合 パッケージ間の衝突が発生する
  6. APTの依存とPythonの依存 それなら… venv を使えばいいんじゃね? ★ LOVOT にデプロイするソフトウェア (daemon) はすべて
 Debian

    パッケージに格納して APT でインストールしている ★ 依存する Python パッケージの必要なバージョンをすべて的確に
 APT でインストールできるとは限らない ★ APT は Python パッケージを全てグローバル環境に置くため
 依存が被った場合 パッケージ間の衝突が発生する ★ 依存パッケージ同士は グローバルでなく 隔離された
 env に住まわせなくてはならない
  7. 例 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を作成・削除する
  8. APTから直接venvを呼ぶ? 確かに問題は解決するけれど… Debian パッケージごとに env が分離できる ⭕ システムの 「どこに」 「いくつ」

    「誰が作った」 env があるか
 わからない ❌ 全員が思ったとおりにenvを作って消してくれる保証がない
 例: 場所や --system-site-packages など ❌ いちいち activate するのが面倒 ❌ Debian Repository にない Python パッケージも導入可能 ⭕
  9. APTから直接pyenvを呼ぶ? pyenvでも確かに問題は解決するけれど… Debian パッケージごとに Python 環境が分離できる ⭕ virtualenvとの連携機能により 決まった箇所にenvを置き
 集中管理できる

    ⭕ pyenv に不慣れな人は極めて事故りやすい ❌ Shell実装で難解 ❌ いちいち activate するのが面倒 ❌ Debian Repository にない Python パッケージも導入可能 ⭕
  10. 開発目標 こういうのが欲しい!! Debian パッケージごとに Python 環境が分離できる ⭕ 決まった箇所にenvを置き集中管理できる ⭕ env

    と実行したいファイルを明示的に指定して一発で実行できる
 (activateが不要) ⭕ Shebangからでも一発で呼べる ⭕ APT や systemd といい感じに連携可能 ⭕
  11. 実現したいこと 実現手法とCLIを考える Debian パッケージごとに Python 環境が分離できる ⭕ $ gxenv create

    foo $ gxenv list /var/lib/python3/envs/foo $ gxenv purge foo 手法 • 設定ファイルで指定されたパスに env を作る • create で作り、list で一覧表示し、purge で消す 決まった箇所にenvを置き集中管理できる ⭕
  12. 実現したいこと 実現手法とCLIを考える $ gxenv run foo pip install requests Collecting

    requests ... 手法 • run サブコマンドを用意し、 env 名・コマンド名・コマンド引数を
 もとにパスを解決して実行 (プロセスの切り替え手法は後述) env と実行したいファイルを明示的に指定して一発で実行できる ⭕ Shebangからでも一発で呼べる ⭕ #!/usr/bin/gxenv run foo python3 print('Hello World!')
  13. 実現したいこと 実現手法とCLIを考える 手法 • APT の postinst, prerm から create/purge

    を呼ぶ • systemd の ExecStart から run を呼ぶ APT や systemd といい感じに連携可能 ⭕
  14. 実現手法と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
  15. 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
  16. 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!')
  17. おさらい • PEP 405で提案され、Python 3.3 より導入された 「仮想環境を
 作成する軽量なメカニズム」 • PyPA

    がホストしていた virtualenv を改良し標準ライブラリに導入 • 標準ライブラリにあるので Python プログラムより呼び出し可能
 (Batteries Included!!) venv とは?
  18. 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 を作るための関数がある モジュール関数
  19. 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の名前
  20. 実行ファイルの見つけ方 実行可能ファイルを見つけるのに手動での 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'
  21. 実行ファイルの見つけ方 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
  22. env 内の実行ファイルをいい感じに走らせる • 別のプログラムを実行するには subprocess を使うことが多い • subprocess は Python

    の子プロセスとして起動されるため
 プロセスツリーにPythonが鎮座する事になる • gxenv が目的のプログラムを起動した時点で仕事を終えているため邪魔 >>> import subprocess >>> subprocess.run(['vim']) subprocess? tmux \_ /bin/bash \_ python \_ vim ps -xf -o cmd ← いらない
  23. ところで Shebang (#!) でよく使われる env コマンドは
 目的のコマンドを起動したあとプロセスツリーから消える $ /usr/bin/env vim

    tmux \_ /bin/bash \_ vim ps -xf -o cmd Shell ← Shell の直下で動いている! gxenv もこれになりたい…どうやって?
  24. 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 (セクション), そしてスタックは、ロードされたプログラムのもので上書きされます。
  25. 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に至るまでのコールスタック
  26. 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が使える)
  27. おさらい • #! で始まるテキストファイルの冒頭行のこと • 解釈方法は統一されていないが 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 とは?
  28. 動作を観察する 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 が有用
  29. gxenv での shebang 実装 argparse に流し込んで gxenv に対する引数とそうでない引数を分ける "gxenv" "run"

    "foo" "python3" "./run" parsed parsed, unknown = parser.parse_known_args() env名 コマンド名 gxenv unknown コマンド引数 あとは 「foo」 という env にある 「python3」 を
 execve して 「./run」を渡す サブコマンド
  30. インテグレーション例 #!/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 インストールされたファイルが削除される前に実行する
  31. インテグレーション例 miku.service systemd の daemon (service) 定義 [Unit] Description=HatsuneMiku [Service]

    ExecStart=/usr/bin/gxenv run miku miku Restart=always [Install] WantedBy=multi-user.target
  32. 開発目標 こういうのが作れた!! Debian パッケージごとに Python 環境が分離できる ⭕ 決まった箇所にenvを置き集中管理できる ⭕ env

    と実行したいファイルを明示的に指定して一発で実行できる
 (activateが不要) ⭕ Shebangからでも一発で呼べる ⭕ APT や systemd といい感じに連携可能 ⭕
  33. gxenv を使う方法 https://pypi.org/project/gxenv/ PyPI にあります。 注意!
 PyPI 版は /usr/bin/gxenv のような実行ファイルを置かないので


    python -m gxenv で CLI を呼ぶ必要があります ♨ 質問があれば Twitter @puhitaku 等でお聞きください