Engineer in Tokyo

PyPI を使わないでデプロイする方法

pipbuildoutなどを使うとデプロイする時にPythonライブラリの依存関係はややこしいことがあります。普段はデプロイスクリプトで、piprequirements.txtを指定して、もしくは、buildoutを実行して、依存ライブラリを落としてインストールしますが、

PyPIがダウンしている場合、環境によって、PyPIにアクセス出来ない場合もありますので、デプロイが止まってしまって困ります。PyPIはダウンしている時にpipはPyPIのミラーを使うことができますが、ミラーに必要がパッケージバージョンが入っていない、ミラーの最後のIDのDNSがちゃんと動いていないときに、pipは当然ちゃんと動かない場合も。Bitbucketや、GitHubからのリポジトリに依存している場合、接続できなかったら、ミラーがないので、当然インストールできます。

つもり、デプロイは外部サイトに依存していて、デプロイを邪魔する問題が出てくる可能性が高いです。

ローカルで必要なライブラリは既にインストールしているので、それを使えばいいじゃん!と自然に思います。実は、情報がなくて、あまり使われてないみたいですが、pipはバンドルを作成する機能があります。バンドルはrequirements.txtの依存ライブラリをzipに固めて、そして、installコマンドで、バンドルからライブラリをインストールしてくれる機能です。つもり、バンドルさえあれば、PyPIにアクセスしたくても良い。

やったぜ! これを使おう

バンドルを作成するのが簡単:

pip bundle -r requirements.txt mybundle.pybundle

インストールも簡単:

pip install mybundle.pybundle

もちろん、virtualenvと組み合わせて使えます。

注意:ファイルの拡張子はpybundleじゃないとinstallコマンドがバンドルを認識してくれない。

バンドルは単のzipファイル:

$ unzip mybundle.pybundle
...
$ ls
build/  pip-manifest.txt  mybundle.pybundle  src/
$ cat pip-manifest.txt
# This is a pip bundle file, that contains many source packages
# that can be installed as a group.  You can install this like:
#     pip this_file.zip
# The rest of the file contains a list of all the packages included:
# These packages were installed to satisfy the above requirements:
Django==1.3
django-debug-toolbar==0.8.4
South==0.7.3
...

デプロイ

デプロイはFabricを使います。ローカルで、バンドルを一回作っておけば、依存ライブラリを修正しない限り、そのまま使えます。

まずは、バンドルを作成するコマンドを作る。runs_once()デコレータで一回しか実行しないようにします。local()メソッドでローカルコマンドを叩きます。

from fabric.decorators import runs_once
from fabric.api import local

@runs_once
def create_bundle():
    u""" 依存ライブラリのバンドルを作り直す """
    local('pip bundle mybundle.pybundle -r requirements.txt')
    print 'Created bundle mybundle.pybundle'

次は、コード自体をアップするコマンドを作ります。下記は、Mercurialをsshでpushしています。

from fabric.api import sudo, cd, local

def _hg_pull():
    local("hg push -r %(revision)s ssh://%(host_string)s/%(base_path)s" % env

def _hg_update():
    with cd(env.base_path):
        sudo("hg update -C -r %(revision)s" % env, user=env.deploy_user)

def push():
    u""" 最新バージョンに更新 """
    _hg_pull(rev)
    _hg_update(rev)

次は依存関係の更新

import os
from fabric.api import sudo, cd, put, local

def update_deps():
    if not os.path.exists('mybundle.pybundle'):
        create_bundle()
    put('mybundle.pybundle', '%(base_path)s/mybundle.pybundle' % env, use_sudo=True)
    with cd(env.base_path):
        sudo('chown %(deploy_user)s:%(deploy_user)s mybundle.pybundle' % env)
        sudo('pip install -E %(venv_path)s mybundle.pybundle' % env)

Mercurialの代わりにrsyncを使う場合はこんな感じで、一発でできる。

from fabric.contrib.project import rsync_project

RSYNC_EXCLUDE=[".hg"]

def push():
    if not os.path.exists('mybundle.pybundle'):
        create_bundle()
    rsync_project(
        env.base_path,
        exclude=RSYNC_EXCLUDE,
        delete=True,
    )
    with cd(env.base_path):
        sudo('chown %(deploy_user)s:%(deploy_user)s mybundle.pybundle' % env)
        sudo('pip install -E %(venv_path)s mybundle.pybundle' % env)

最後は、deployコマンドを作る

def deploy():
    push()
    update_deps()

    # DB を更新
    migrate_db()

    # ウェブサーバーを再起動
    reboot_server()

こういう感じで、ローカルとサーバーの接続さえできれば、デプロイできます。外部サイトに依存したいのが楽過ぎて、逆にいい意味で困ります。