gunicornをsupervisorで制御しつつhot deployできるようにする

タイトルの通り、gunicornなWebアプリをsupervisorで制御しつつhot deployできるようにしたメモです。

登場人物

問題

  • gunicornのhot deployを利用する場合、直接はsupervisorの制御下に置けない
    • gunicornはhot deployの仕組みを持っている
    • 流れ
      • gunicornのmasterプロセスにUSR2シグナルを送ると、新しいmasterプロセスを産む
        • つまり新masterの親プロセスは旧master
      • 旧masterと旧workerは生き続けているので、ほどよい頃合いで旧masterにQUITシグナルなどを送って死んでもらう
      • 新masterの親が死んだので、init (PID 1) が新masterの親となる
    • というわけで、supervisor視点だとgunicorn(旧master)が死んだのでまた立ち上げようとするが、新masterはinitを親として起動しているので競合しちゃう次第

結論

  • supervisor + unicornherder + gunicorn

案1: start_server配下でgunicornを立ちあげ、hot deployはstart_serverの機能を使う

## /etc/supervisor/conf.d/oreno.conf
[program:oreno]
command=/home/hirose31/oreno/script/start
directory=/home/hirose31/oreno
...
## /home/hirose31/oreno/script/start
#!/bin/bash

export PYENV_ROOT="/home/hirose31/oreno/pyenv"
export PATH="$PYENV_ROOT/bin:$PATH"
eval "$(pyenv init -)"

exec start_server \
     --port 127.0.0.1:1919 \
     --signal-on-term=TERM \
     --signal-on-hup=QUIT \
     --kill-old-delay=10 \
     -- \
     /home/hirose31/oreno/script/start-gunicorn
## /home/hirose31/oreno/script/start-gunicorn
#!/bin/bash

if [[ -n "$SERVER_STARTER_PORT" ]]; then
  GUNICORN_FD=
  for l in $(echo $SERVER_STARTER_PORT | tr ';' ' '); do
    GUNICORN_FD="$GUNICORN_FD $(echo $l | cut -d= -f2)"
  done
  export GUNICORN_FD=$(echo $GUNICORN_FD | tr ' ' ',')
fi

exec /usr/local/bin/envdir /home/hirose31/oreno/env \
  gunicorn -w 3 --reuse-port --capture-output oreno:application

これで supervisorctl signal HUP oreno で start_server に HUP を送ると、start_serverが新たにgunicornを起動して、10秒後に古いgunicornにQUITを送って殺してくれる。はず。

つまり、(gunicornの機能ではなく)start_serverの機能でhot deployが実現できる。はず。

start_server に対応するにはいくつかの要件が必要ですが、

UnicornUNICORN_FD と同様のものが gunicorn には GUNICORN_FD という環境変数名で存在するので、gunicornもstart_serverに対応できる。はず。

そう。はず、なのであった!

gunicornの このpull request で、GUNICORN_FD を評価するところのがこのような条件付きになり、

### gunicorn/arbiter.py: start
            elif self.master_pid:
                fds = []
                for fd in os.environ.pop('GUNICORN_FD').split(','):
                    fds.append(int(fd))

初っ端の起動時は master_pid は 0 なので GUNICORN_FD は評価されないのであった!!

条件を elif os.environ.get('GUNICORN_FD', ''): にすれば期待通り動くことは確認できたんだけど、できればgunicornに手を入れないで使いたいのでこの案はとりあえずボツに…

将来、外部由来の GUNICORN_FD を評価するようになったら、gunicornは--configオプションでpythonの文法で設定が書かれたファイルを指定することができるので、そこで SERVER_STARTER_PORT を参照して GUNICORN_FD をセットする処理を書いたり、pre_fork, post_fork hookを使って、Server::Starter で Unicorn を起動する場合の Slow Restart - sonots:blog と同じことも実現できるんじゃないかと思います。

余談ですが、start_serverserver-prog の指定は、ラップしたスクリプトのみ、オプション指定無しにするのをお勧めします。

例えば start_server -- /my/app -p 8 で起動していて -p 16 に変えたいときは、start_server 自体の再起動が必要(つまりhot deployできない)になってしまいます。これがラップしていて start_server -- /my/app_wrapper で起動している場合は、app_wrapper の中を書き換えて start_server に HUP を送ればOKになります。

start_server -- envdir ./env sh -c 'exec /my/app -p ${MAX_PROCESS:-8}' と書けばHUPで引数の値を(envdir経由で)変えることは可能ですが、やや技巧的だしオプションの種類の増減には対応できないので、素直にラッパースクリプトを用意した方がよいでしょう。

案2: unicornherder配下でgunicornを立ちあげ、hot deployはgunicornの機能を使う

「gunicorn supervisor」でググるとみなさんお困りのようで、対応として rainbow-saddleunicornherder がよく挙げられています。

rainbow-saddleは「cannot actively maintain it」だそうなので、unicornherderを試してみます。

## /etc/supervisor/conf.d/oreno.conf
[program:oreno]
command=/home/hirose31/oreno/script/start
directory=/home/hirose31/oreno
...
## /home/hirose31/oreno/script/start
#!/bin/bash

export PYENV_ROOT="/home/hirose31/oreno/pyenv"
export PATH="$PYENV_ROOT/bin:$PATH"
eval "$(pyenv init -)"

exec unicornherder -o 10 -p /var/run/oreno.pid -- \
    -w 3 --reuse-port --capture-output oreno:application

unicornherderの流れはこんな感じです。

  • gunicornを--pid FILE --daemonつきで起動する
    • masterのPIDは所定のファイルに書き出される
      • unicornherderの--pidfile PATHでPIDファイルを指定しないと、cwdにPIDファイルが作られるので注意
    • gunicornはunicornherderの元を離れ、initの子プロセスとなる
  • unicornherderはHUPを受けると、gunicornのmasterにUSR2を送る
  • gunicornのhot deployが開始され、新masterが誕生する
    • gunicornによってPIDファイルが更新される
  • unicornherderはPIDファイルを2秒間隔でポーリングして、masterの変化を検知する
  • masterの変化を検知すると、
    • overlap (デフォルト120秒) の時間だけsleepする
      • この間は新旧workerが混在する
    • 後、旧masterを殺す
      • masterにWINCH, QUITシグナルを送る
      • QUITを受けたworkerは処理中であっても即、死ぬ

unicornherderはgunicornを子プロセスとしておかず、gunicornはinitの子プロセスとなる点に最初「オッ!」っと思いましたが、これは古き良きdaemonしぐさですからまぁ違和感はないです。

これで一応、問題は解決できたんですが、unicornherderはgunicornコマンドでのgunicornの起動しかサポートしていません。

gunicornの起動方法、Pythonの各種WAFとの連携方法は gunicornコマンド以外にもあります。例えば、Bottle だと bottle.run()server='gunicorn' を与えると、gunicornが起動します。

その場合は、unicornherderは--gunicorn-bin, -gオプションでgunicornのフルパスを指定できるのでこれを使います。

## /home/hirose31/oreno/script/start
#!/bin/bash

export PYENV_ROOT="/home/hirose31/oreno/pyenv"
export PATH="$PYENV_ROOT/bin:$PATH"
eval "$(pyenv init -)"

exec unicornherder -o 10 -p /var/run/oreno.pid \
    -g /home/hirose31/oreno/script/oreno-gunicorn \
    --

oreno-gunicorn はお好みの方法でgunicornなプロセスを立ち上げる処理を書いたスクリプトです。

## /home/hirose31/oreno/script/oreno-gunicorn
#!/bin/bash

exec /home/hirose31/oreno/bin/oreno-python /home/hirose31/oreno/omaeno.py

注意点は以下の通りです。

  • unicornherderの-pオプションで指定したPIDファイルと、gunicornが書き出すPIDファイルを一致させる
  • gunicornはdaemonモードで起動するようにする

おわりに

unicornherderを使えば目的が達成できることがわかりましたが、個人的に安心と信頼と実績のある start_server が使いたい今日この頃です。

DropboxからGoogle Driveに乗り換えた

以前から Mac 複数台、Linux 複数台、iPhoneDropboxを使ってきたのだけど、最近マシンを交換したら3台制限に引っかかって同期できなくなってしまったので、Google One を契約してるのもあり無難なところでGoogle Driveに移行することにした。

方針

  • Googleドライブ直下に Sync というフォルダを作って、このフォルダだけ同期する (以前の ~/Dropbox/ 的な感じ)

セットアップ

macOSはまぁ普通にダウンロードしてインストールして設定すればいいんだけど、Googleドライブ直下のファイルの同期を抑止する方法がわからんかった。

LinuxGUIは必要ないのでGrive2を使うことにした。

0.5.1-dev(2019-07-12現在未リリース)から .griveignore ファイルで同期する対象の指定ができるようになったのでこれを使う。

ビルドとインストールはドキュメントの通り(ソースはリリースページのじゃなくて、git cloneするかmaster.zipを使う)ににして、初回実行はこんな感じ。

$ grive --version
grive version 0.5.1-dev Jul 11 2019 18:10:47

$ mkdir ~/GoogleDrive/ && cd ~/GoogleDrive/

$ cat << EOF > .griveignore
*
!Sync
EOF

$ grive -a

これで Sync だけが同期される。

griveは同期が終わると終了して常駐はしないので、定期的に、更新があったときに同期が実行されるようにする。

今回はホームディレクトリ下のディレクトGoogleDrive を基点ディレクトリとしたので、一般ユーザーで次のように仕込めばよい。

systemctl --user enable grive-timer@$(systemd-escape GoogleDrive).timer
systemctl --user start grive-timer@$(systemd-escape GoogleDrive).timer
systemctl --user enable grive-changes@$(systemd-escape GoogleDrive).service
systemctl --user start grive-changes@$(systemd-escape GoogleDrive).service

ログは journalctl で確認できる。

journalctl -f --user -u grive-timer@GoogleDrive.timer
journalctl -f --user -u grive-changes@GoogleDrive.service

というわけで今までありがとうDropbox

今回のおハマり

sshで入った先で systemctl --user がエラーになるときは、

$ systemctl --user
Failed to connect to bus: No such file or directory

sshd_configでUsePAM yesになっているか確認しましょう。

pam経由からのsystemd-logindからのログインじゃないのでセッションが作られません。

あわせて読みたい

Ubuntu 18.04 LTSからAMIを作る前にやらなければならないこと

AWS Marketplace の Ubuntu 18.04 LTS 20190514 からインスタンスを立てて、そのインスタンスからAMIを作る前には次のことをやらなければなりません。

ifupdownを削除する

sudo apt purge ifupdown

ifupdownパッケージがインストールされている状態で作ったAMIで起動すると、起動はするもののNICが見つけられずに一切のネットワークアクセスができないインスタンスになってしまいます。

/etc/netplan/50-cloud-init.yaml は削除する必要はなく、ifupdown がインストールされていなければ、新規インスタンスMAC アドレスにあわせて適切な設定ファイルが生成されました。

深追いしてませんが、ifupdown がインストールされていると、ifupdownに代わる新しいネットワーク管理システムの netplan が NIC を見つけられない/見つけようとしないんじゃないかと思います。

ちなみに ifupdown に依存しているパッケージはまだ結構あって( apt-cache rdepends ifupdown 調べ)、例えば ifenslave をインストールすると ifupdown もインストールされたりします。

なお、一度起動してしまえば ifupdown をインストールしても問題ありません。が、その後で /etc/netplan/50-cloud-init.yaml を削除して再起動すると、NIC がみつけられないインスタンスになってしまい詰むので注意してください。言い換えると、適切な /etc/netplan/50-cloud-init.yaml が存在するならば、ifupdown をインストールしても問題ありません。

/etc/machine-id を0バイトのファイルにする

: | sudo tee /etc/machine-id

/etc/machine-id にはそのホスト固有のIDが格納されています。

/etc/machine-id がある状態で作ったAMIで起動すると、同じIDを持ったインスタンスが作られてしまいます。

なので /etc/machine-id を初期状態にする必要があるのですが、先に示したように、0バイトのファイルにしなければなりません。

sudo rm -f /etc/machine-id でファイルを消したり、sudo bash -c 'echo > /etc/machine-id' で改行1バイトのみファイルにした場合は、起動時に再生成されないので注意してください。

CentOS 6でPython 3でTensorFlowを使う方法、もしくはdynamic linkerとshared objectの差し替え

Python 3

CentOS 6のopensslのバージョン(1.0.1e)の関係で、Python 3.7以上はビルドが失敗します。

今回は3.7にこだわりはないので、pyenvでPython 3.6の最新の3.6.8をインストールして、pipでtensorflowをインストールします。詳細は割愛ググってね。

glibc, stdc++

インストールに成功しても、使おうとするとエラーが出ます。

$ python3.6 -c 'import tensorflow; print("hello")'
Traceback (most recent call last):
...
ImportError: /lib64/libc.so.6: version `GLIBC_2.16' not found (required by /home/ore/.pyenv/versions/3.6.8/lib/python3.6/site-packages/tensorflow/python/_pywrap_tensorflow_internal.so)
...

$ ldd /home/ore/.pyenv/versions/3.6.8/lib/python3.6/site-packages/tensorflow/python/_pywrap_tensorflow_internal.so |& grep 'not found' | awk '{print $4}'
`GLIBC_2.16'
`GLIBC_2.14'
`GLIBC_2.17'
`GLIBC_2.15'
`GLIBCXX_3.4.15'
`GLIBCXX_3.4.19'
`GLIBCXX_3.4.14'
`CXXABI_1.3.7'
`GLIBCXX_3.4.17'
`GLIBCXX_3.4.18'
`CXXABI_1.3.5'
`GLIBC_2.14'
`GLIBC_2.16'
`GLIBC_2.17'
`GLIBCXX_3.4.14'
`GLIBCXX_3.4.18'
`CXXABI_1.3.5'
`GLIBCXX_3.4.15'
`GLIBCXX_3.4.19'
`GLIBCXX_3.4.17'
`CXXABI_1.3.7'

どようやらglibc 2.17とlibstdc++ 3.4.19が必要そうです。

さすがにシステムのglibcを置き換えるのは怖いので、別のディレクトリに展開して今回のpython3.6はそこのshared objectを使うようにします。

glibcとlibstdc++は、https://pkgs.org/ で探してCentOS 7のを拝借します。

$ cd ~/tmp/
$ wget \
  http://mirror.centos.org/centos/7/updates/x86_64/Packages/glibc-2.17-260.el7_6.5.x86_64.rpm \
  http://mirror.centos.org/centos/7/updates/x86_64/Packages/glibc-devel-2.17-260.el7_6.5.x86_64.rpm \
  http://mirror.centos.org/centos/7/os/x86_64/Packages/libstdc++-4.8.5-36.el7.x86_64.rpm \
  http://mirror.centos.org/centos/7/os/x86_64/Packages/libstdc++-devel-4.8.5-36.el7.x86_64.rpm \
  ;

~/lib/ の下に展開します。

$ cd ~/lib/
$ for p in ~/tmp/*.rpm; do echo $p; rpm2cpio $p | cpio -idv; done
$ ls -F
etc/  lib64/  sbin/  usr/  var/

以下、パス指定が長くなるので簡略化のため変数に入れときます。

py36="$HOME/.pyenv/versions/3.6.8/bin/python3.6"
pywrap_tensorflow_internal="$HOME/.pyenv/versions/3.6.8/lib/python3.6/site-packages/tensorflow/python/_pywrap_tensorflow_internal.so"

my_ld="$HOME/lib/lib64/ld-linux-x86-64.so.2"
my_libs="$HOME/lib/usr/lib64:$HOME/lib/lib64"

ld-linux.so はコマンドとしても実行できて、 --library-path でdynamic loadするshared objectのパスを指定できます。環境変数 LD_LIBRARY_PATH でも同様のことができますが、作用範囲が子プロセスまで及ぶかどうかという違いがあります。

ld-linux.so 経由で python3.6 を起動して、先程エラーになったコードを実行してみます。

$ $my_ld --library-path $my_libs $py36 -c 'import tensorflow; print("hello")'
hello

今度は正常終了しました!やったネ!!

child process

早速、最終的に走らせたいスクリプトを実行してみると…

$ $my_ld --library-path $my_libs $py36 oreno.py
* Serving Flask app "server" (lazy loading)
* Environment: production
...
Traceback (most recent call last):
...
ImportError: /lib64/libc.so.6: version `GLIBC_2.16' not found (required by /home/ore/.pyenv/versions/3.6.8/lib/python3.6/site-packages/tensorflow/python/_pywrap_tensorflow_internal.so)
...

失敗しました。

エラーメッセージからすると、システムのglibcをロードしてしまっているようです。strace -f -s 1000 $my_ld ... で確認してみると、後半で $my_ld を介さないで $py36execve して異常終了していました。詳細は確認してませんが、どっかで fork してるんじゃないかと思います。

子プロセスにも新しいglibcの場所を教えてあげればよいので、試しに LD_LIBRARY_PATH をセットして実行してみると…

$ env LD_LIBRARY_PATH=$my_libs $my_ld --library-path $my_libs $py36 oreno.py
...
/home/ore/.pyenv/versions/3.6.8/bin/python3.6: relocation error: /home/ore/lib/lib64/libc.so.6: symbol _dl_starting_up, version GLIBC_PRIVATE not defined in file ld-linux-x86-64.so.2 with link time reference

失敗しました。

たぶん、システムの /lib64/ld-linux-x86-64.so.2 で新しいglibcをロードしようとして失敗しているんだと思います。

もし、成功したとしても、LD_LIBRARY_PATH は作用範囲が大きいので、他の方法があれば避けたい手段です。

patchelf

Linuxの実行ファイル形式のELFには、dynamic linker (interpreter) とライブラリのサーチパス(runpath)を埋め込むことができます。これらは readelfpatchelf で確認することができます。

CentOS 6用の patchelf はEPELにあります。

$ readelf -a /usr/bin/mailq | grep -e interpreter -e runpath
      [Requesting program interpreter: /lib64/ld-linux-x86-64.so.2]
 0x000000000000001d (RUNPATH)            Library runpath: [/usr/lib/postfix]

$ patchelf --print-interpreter --print-rpath /usr/bin/mailq
 /lib64/ld-linux-x86-64.so.2
 /usr/lib/postfix

patchelf はこれらの情報を変更することができるので、$py36interpreterとrunpathを変更してしまえば、 $my_ld --library-path $my_libs の前置が不要になるというわけです。

やってみましょう。

$ cp -p $py36 ${py36}.bak
$ patchelf --set-interpreter $my_ld --set-rpath $my_libs $py36

さっそく、$my_ld の前置なしで実行してみると…

$ $py36 -c 'import tensorflow; print("hello")'
Traceback (most recent call last):
...
ImportError: /usr/lib64/libstdc++.so.6: version `GLIBCXX_3.4.15' not found (required by /home/ore/.pyenv/versions/3.6.8/lib/python3.6/site-packages/tensorflow/python/_pywrap_tensorflow_internal.so)

エラーが出ました。が、これは想定内です。

$py36 は期待したとおり新しいglibcをロードできたのですが、_pywrap_tensorflow_internal.so がlibstdc++をロードしようとして新しいのが見つけられずエラーになっているわけです。(glibcは既に $py36 が新しいのをロード済みなので大丈夫)

なので、_pywrap_tensorflow_internal.so もrunpathを書き換えてしまいましょう。

_pywrap_tensorflow_internal.so は既にrunpathが設定されているので、それに追加する感じでセットします。(純粋なshared objectなのでinterpreterの変更の必要はありません)

$ORIGIN が展開されないようにシングルクォートするのを忘れないでください。

$ patchelf --print-rpath $pywrap_tensorflow_internal
$ORIGIN/../../_solib_k8/_U_S_Stensorflow_Spython_C_Upywrap_Utensorflow_Uinternal.so___Utensorflow:$ORIGIN/:$ORIGIN/..

$ cp -p $pywrap_tensorflow_internal ${pywrap_tensorflow_internal}.bak
$ patchelf --set-rpath '$ORIGIN/../../_solib_k8/_U_S_Stensorflow_Spython_C_Upywrap_Utensorflow_Uinternal.so___Utensorflow:$ORIGIN/:$ORIGIN/..'":$my_libs" $pywrap_tensorflow_internal

さて、

$ $py36 -c 'import tensorflow; print("hello")'
hello

$ $py36 oreno.py
* Serving Flask app "server" (lazy loading)
* Environment: production
...
INFO:werkzeug: * Running on http://127.0.0.1:5000/ (Press CTRL+C to quit)
...

ようやく正常に起動できました!!!

が、socket.getaddrinfo() が失敗して名前が引けず…

まとめ

  • ld-linux.so はコマンドとしても実行できて、 --library-path でdynamic loadするshared objectのパスを指定できる(環境変数 LD_LIBRARY_PATH に比べ作用範囲を限定的にできる)
  • ELFには、dynamic linker (interpreter) とライブラリのサーチパス(runpath)を埋め込むことができ、patchelf コマンドで変更できる

ところで、

debootstrap で bionic の環境を作ってまるっとCentOS 6にコピーして chroot した方がよかったかもしれん… 試してないけど動くんじゃないかと…

kernelが古くて残念、ダメでした!

# chroot ./bionic /bin/bash
FATAL: kernel too old

# file bionic/bin/bash
bionic/bin/bash: ELF 64-bit LSB shared object, x86-64, version 1 (SYSV), dynamically linked (uses shared libs), for GNU/Linux 3.2.0, stripped

メインマシンをUbuntu 14.04から18.04にしてみた

Ubuntu 14.04 (trusty) が 2019-04-30 で EOL になる

Ubuntu 14.04 LTS Trusty Tahr Extended Security Maintenance | Ubuntu blog

のと、day jobの方でも18.04 (bionic)を使う予定なので、メインマシンのtrustyをbionicにしてみた。

Ubuntu

に書いてある通り、do-release-upgrade 実行するだけ。楽ちん。一足飛びにはいけないので、trusty→xenial、xenial→bionic した。

アップグレード自体は特に問題なかった。

グラフィカルログインの無効化

漢は黙ってコンソールログイン派なのでlightdmとかgdmとかそういうのを無効化する。

systemctl set-default multi-user.target

dig

デフォルトで +edns するようになったようで、一部のDNSサーバーがFORMERRを返して名前が引けないので無効化した。

echo '+noedns' > ~/.digrc

Emacs

なんとなく勢いで24.4から26.1にしてみた。

ファイルが意図しないwindowで開く

例えば、1つのframeを上下2つのwindowに分割してて、上のwindowでfind-fileすると下のwindowで開く、とか謎挙動しだした。

原因は popwin の古い設定

(setq display-buffer-function 'popwin:display-buffer)

が残っていたせいで、これを削除したらOKになった。

さようならsense-region、こんにちはsense-expand-region

sense-region.elをロードすると警告が出るようになった。

Warning (bytecomp): ‘interactive-p’ is an obsolete function (as of 23.2); use ‘called-interactively-p’ instead.
Warning (bytecomp): ‘mapcar’ called for effect; use ‘mapc’ or ‘dolist’ instead [16 times]

動作はするけど、sense-region.el自体、配布元にアクセスできなくなって久しいので、結局、sense-expand-region.el に乗り換えました。

sense-region.elと同じように C-SPC に割当。

(global-set-key (kbd "C-SPC") 'sense-expand-region)

最初 emacs-jp Slack の人たちに相談したら、矩形選択は標準の rectangle-mark-mode を、選択リージョンの拡張は expand-region を教えてもらいました。(あざます!!)

expand-region は最初の発動でいきなり範囲選択になる(sense-regionはset-mark-command)のに慣れなくて、ぐぐったら sense-expand-region.el を見つけて、なんとこれで矩形選択もできるので乗り換えた次第。

synergy

今まで

  • server
    • Ubuntu trusty
    • synergys 1.4.12
  • client
    • macOS Mojave (10.14.3)
    • synergyc 1.3.1 (数年前に昔のmacOSで自ビルドしたやつ)

で問題なく使えてたんだけど、bionicのsynergys 1.8.8 にしたらキーボードとマウスは共有されるんだけど、クリップボードが共有されない。これが思いの外、ストレスマックスボンバー。

1.8.8のバイナリ配布

https://sourceforge.net/projects/synergy-stable-builds/

から macOS 版をダウンロードして試したら、マウスとクリップボードは共有されるけど、なんとキーボードが共有されない!!

server, client共に最新の1.10.1(今回の件で買った!)にしても、キーボードが動かない。

trustyで動いてた1.4.12をbionicでビルドしたところ、たぶん libcrypto++ のシグネチャが違うかなんかでビルドできず。

他にもdebootstrapでtrustyの環境作ってchroot実行してみた(SEGV!)り、他のバージョンでも試したりいろいろやったけどダメで、結局、trustyからsynergysとlibcrypto++.so.9.0.0をコピーしてきて、

#!/bin/sh

exec env LD_LIBRARY_PATH=/usr/local/app/synergy-1.4.12/lib \
     /usr/local/app/synergy-1.4.12/bin/synergys "$@"

な感じで立ちあげて無事、全部共有できて一旦めでたしめでたし。

でも、macOSの方の1.3.1は、この前MacBookを乗り換えたときにソースコード消しちゃったようだし、ソースがあってもCarbonのヘッダーファイルないとコンパイルできないと思うのでもう最新のXcodeではビルドできないので、本当はせっかく買った 1.10 で動くのがいいんだけど。。。

PythonのClickのサブコマンドをsymlinkで表現する作例

PythonClick

command [--debug] foo [--force] [--yes]

なのを実装したとして、これと同じのを command-foo というsymbolic linkを作って

command-foo [--debug] [--force] [--yes]

でも実行できるようにしたい作例。

command-foo --force --debug と実行するとエラーになるのがイマイチ。。。
他にいい方法があったら教えてください!!

REST API フレームワーク Connexion のススメとその作例

Python の Connexion というフレームワークとそのサンプルアプリケーションを書いたのでその紹介です。


Connexion は「API (spec) First」を謳うフレームワークです。

API First」とは、 ご存じ The Twelve-Factor App の追補として 2016 年に Pivotal 社が提唱したガイドライン Beyond the Twelve-Factor App の中のひとつです。

簡単にいうと、コードを書き始める前にまずAPIの仕様を定める。たとえば昨今のマルチデバイス対応のような複数チームで開発を進めるような現場で、お互いこのAPI仕様を拠り所とすることで、他方の開発プロセスに干渉することなく、円滑に開発を進めることができる、というものです。 (たぶん)

Connexion はこの API First を実践するのを助けてくれるプロダクトで、OpenAPI (過去に Swagger spec と呼ばれていたものです)で記述した仕様を軸として次のことが実現できます。

  • API 利用者向けのドキュメントを生成できる
  • 仕様に記述した入力パラメータの validation ルール(必須パラメータのあり/なしとか、正規表現で記述した値の形式チェックとか)を自動で実施してくれるので、適用コードを一切書く必要がない

現職で API サーバーを 3 つ実装して、今は Connexion で 4 つめを実装しているのですが、ドキュメントと実装の乖離は頭の痛い問題でした。具体的にいうと、値の形式チェックのルールをちょっと変えたんだけどドキュメントに反映しわすれていたとか、初期に書いたドキュメントの一部が実装から漏れていたとか、みなさんも容易に想像できるんじゃないでしょうか。

また、エンドポイントの情報(HTTPのメソッドとパス)をドキュメントと controller を呼ぶ router の両方に書かないといけないのが常々無駄だと思っていたのですが、Connexion が仕様を読んで所定の controller のメソッドを呼んでくれるので router を記述する必要がなくなったのもよい点の1つです。

Connexion を使うことでこういった問題から解放されたので本当に助かっています。

ほかにも API の動作をちょっと試したりデモするのに便利な Web UI のダッシュボードがついていたり、OAuth 2 のトークンベースの認証に対応していたりといろいろと特徴があるので詳しくは Connexion のサイトをみてみてください。


そんなこんなでここしばらく Connexion の試し書きをしていました。

などのサンプル実装を大いに参考にさせてもらったのですが、もうちょっと実践的な作例を書いてみました。

  • データストアは RDBMS で、 SQLAlchemy ORM の declarative mapping を使用
  • Connexion (の内部の Flask )との連携は Flask-Alchemy を使用
  • ORM のシリアライズ(dict化)には sqlathanor を使用
  • サンプルとして 2 つのテーブル(リソース)を定義して、one-to-many のリレーション
  • 検索条件の処理は、所定の SQL っぽい記法(JSON)でリクエストをすると、いい感じに SQLAlchemy の query オブジェクトを返してくれる簡易 query builder を書いた
  • pytest と WebTest を使ってテストも用意した

まだまだ Python は B- ぐらいウデマエの上に SQLAlchemy なども使ったことがなかったので、「こう書いたほうがいいでし!」とかあったらぜひ プルリ お待ちしています!