FabricでPlay! frameworkアプリのデプロイを自動化してみた


随分前から、勉強がてら個人でちょこちょこwebサイト作ってる。デプロイはjarファイルのコピーだけとは言え、毎回手動でdeployとサービス再起動をするのはさすがに面倒になってきたので、デプロイを自動化するツールを導入する事にした。

環境等

  • Play! framework 2.2
  • web(app)、DB、バッチでサーバーが分かれている
  • デプロイ先はいずれもLinux
  • ローカル開発環境はOS X Mavericks (10.9)
  • Fabric 1.8.1を使用

最初Capistranoにしようとしたがやめた

仕事先のプロジェクトで使っているCapistranoを最初使おうとした。でも、Capistranoは少し試したものの以下の理由で止めた。(Ruby on Railsの場合はCapistranoが一番良い選択肢だとは思うけど。)

  • 最新バージョンのCapistrano 3は、ドキュメントが非常に少ない。
  • サーバー上でgit cloneを実行してそれをdeployする、というのが想定されている使い方なので、ローカル環境でビルドしてjarをdeployするというユースケースだと、使いにくい(というか使わない機能が多い)。
  • こちらのページで紹介されているように、デフォルトのタスクを削除して、シンプルなタスクを定義して使うというやり方もありだが、「デフォルトのタスクを削除」というのが無駄な気がする。

ということで、ちょっとググった結果、Python製のFabricというツールはCapistranoより薄いシンプルらしいので、そっちを使ってみる事にした。

ローカル環境にFabricのインストール

OS X Mavericksの場合、最初からpython 2.7.5が入っているようなので、こっちを使う事にした。Homebrewで環境作ると、最初から入ってるpythonとごちゃ混ぜになりそうなのでやめた。ちなみに、自分のpythonの知識は戦闘力5程度なので、以下は参考程度に。

pip, setuptoolsのインストール

pythonのパッケージ管理ツールであるpipをインストール。読み方は「ぴっぷ」らしいが、どうしてもカワイイ系のポケモンを思い出してしまう。

$ sudo easy_install pip

setuptoolsというのも必要らしいので、こちらもドキュメントに従ってインストール。

$ wget https://bitbucket.org/pypa/setuptools/raw/bootstrap/ez_setup.py -O - | sudo python

Fabricのインストール

後は、pipを使ってFabricをインストール

$ sudo pip install fabric

Fabricを使ってみる

まずはプロジェクトのルートディレクトリにfabfile.pyというファイルを作成。とりあえずチュートリアルに沿って触ってみればどんなものか分かるかと思う。

まずは基本のコマンド(?)4つを覚えればOK

まずは以下の4つのコマンド(+pythonの基本の文法)を覚えると、大体の事は出来るようになる。

  • local : ローカル環境でコマンドを実行
  • run : リモート環境でコマンドを実行
  • put : ローカルからリモートにファイルをコピー
  • execute : 別のタスクを実行

上の4つを使った(簡略化した)fabfile.pyは以下のようになる。

from fabric.api import local, run, env

env.user = "playuser"
env.hosts = ['web1', 'web2']

def package():
    local('play package')

def deploy():
    execute(package)
    put('target/scala-2.10/project-foo_2.10-0.1-SNAPSHOT.jar', '/path/to/lib')
    run('/etc/init.d/play restart')

packageとdeployという2つのタスクが定義され、deployの前にはpackageが実行される。すごい簡単にデプロイのタスクが書けるのが分かると思う。

タスクの実行は以下の通り。

$ fav deploy

envって何?

上のfabfile.pyでは、env.userとenv.hostsというのを定義している。envというのはFabricにおける環境変数的な役割のディクショナリで、env.userやenv.hostsのように決められた役割のものもあるし、自分で勝手に定義する事も出来る。

ユーザー定義のenvは、タスク間で情報を共有する際に使う。

Fabricで使うenvのエントリーは、env.user、env.hosts以外にも沢山あるが、使うものを徐々に覚えていけば良い。

FabricでPlay!用のデプロイを記述

env.roledefsで複数ロールの定義

今回はWeb、DB、バッチでそれぞれサーバーが異なり、それぞれに違った内容のデプロイを実施したい。そんな時に使うのが、env.roldedefs。具体的には以下のように定義する。

env.roledefs = {
    'web': ['web.example.com'],
    'db': ['db.example.com'],
    'batch': ['batch.example.com']
}

で、以下のように定義しておくと、

@roles('web')
def web_deploy():
    put('local-file.jar', 'remote-path/')

web_deployタスクが実行された場合に、web.example.comに対してデプロイされる。

シンボリックリンク作成、ロールバック

Capistranoだと、デプロイ時にreleasesディレクトリの下にタイムスタンプでディレクトリが掘られ、そこに対してファイルがデプロイされ、その後、そのディレクトリに対してcurrentというシンボリックリンクが張られる(詳しくはこの辺を参照)。

Fabricの場合、その辺は手動でやらなければいけないみたいだけど、こちらのgistを参考にしたら(というかほぼそのまま)結構簡単に書けた。

def releases():
    env.base_dir = "/home/playuser"
    env.current_path = "%(base_dir)s/current" % { 'base_dir':env.base_dir }
    env.releases_path = "%(base_dir)s/releases" % { 'base_dir':env.base_dir }
    env.shared_path = "%(base_dir)s/shared" % { 'base_dir':env.base_dir }

    env.releases = sorted(run('ls -x %(releases_path)s' % { 'releases_path':env.releases_path }).split())
    if len(env.releases) >= 1:
        env.current_revision = env.releases[-1]
        env.current_release = "%(releases_path)s/%(current_revision)s" % { 'releases_path':env.releases_path, 'current_revision':env.current_revision }
    if len(env.releases) > 1:
        env.previous_revision = env.releases[-2]
        env.previous_release = "%(releases_path)s/%(previous_revision)s" % { 'releases_path':env.releases_path, 'previous_revision':env.previous_revision }

def deploy():
    from time import time
    env.current_release = "%(releases_path)s/%(time).0f" % { 'releases_path':env.releases_path, 'time':time() }

    run("mkdir -p %(target_dir)s" % { 'target_dir':env.current_release })
    put('local-file.jar', env.current_release)

def rollback_code():
    if not env.has_key('releases'):
        execute(releases)
    if len(env.releases) >= 2:
        env.current_revision = env.releases[-1]
        env.previous_revision = env.releases[-2]
        env.current_release = "%(releases_path)s/%(current_revision)s" % { 'releases_path':env.releases_path, 'current_revision':env.current_revision }
        env.previous_release = "%(releases_path)s/%(previous_revision)s" % { 'releases_path':env.releases_path, 'previous_revision':env.previous_revision }
        run("rm %(current_path)s; ln -s %(previous_release)s %(current_path)s && rm -rf %(current_release)s" % { 'current_release':env.current_release, 'previous_release':env.previous_release, 'current_path':env.current_path })

Play!の起動

ここはFabricとはあまり関係ない話。

以下のコマンドで、パッケージとか起動スクリプトが作成される。詳しくはplayのドキュメントを参照。

$ play stage

で、作成されたtarget/universal/stage以下のbin, conf, libをリモートサーバーのsharedディレクトリーに放り込む。

ただし、lib/以下から自分のプロジェクトのjarファイルだけは取り除いておく。自分のプロジェクトのjarファイルは、毎回releases/(タイムスタンプ)ディレクトリにコピーされるので。

※ここでは、dependenciesはあまり更新されない事を前提としている。

あとは、bin以下の起動スクリプトを少し変更する。以下のようになっている部分を、

declare -r lib_dir="$(realpath "${app_home}/../lib")"
declare -r app_mainclass="play.core.server.NettyServer"

declare -r app_classpath="$lib_dir/projectfoo.projectfoo-1.0-SNAPSHOT.jar:$lib_dir/3rdparty-lib.jar:....."

以下のようにする。

declare -r lib_dir="$(realpath "${app_home}/../lib")"
declare -r app_mainclass="play.core.server.NettyServer"
declare -r deploy_dir="$(realpath "${app_home}/../../current")" # 追加

declare -r app_classpath="$deploy_dir/projectfoo.projectfoo-1.0-SNAPSHOT.jar:$lib_dir/3rdparty-lib.jar:....."

これで、以下のように起動できる。

$ shared/bin/projectfoo-website -Dconfig.file=shared/conf/application-production.conf

プロセスの停止

ここまで来るとあとちょい。deploy後に、Play!アプリケーションを再起動したい。

停止は、RUNNING_PIDというファイルに書かれているPIDのプロセスをkillすれば良いので、こんな感じ。runの引数でshell=Falseを付けておかないと、バッククォートとかがエスケープされてしまう。

def web_stop():
    run("kill `cat %(shared_dir)s/RUNNING_PID`" % {"shared_dir":env.shared_path}, shell=False)

プロセスをバックグラウンドで起動

Fabricでプロセスをバックグラウンドで実行するにはちょっとしたハックが必要っぽい。

ポイントは以下の3点。

  • pty=False にする。
  • &をつけてバックグラウンドにまわす。
  • 標準出力、標準エラー出力には何も出力しない。

具体的には以下のようにする。

def web_start():
    run("shared/bin/projectfoo-website -Dconfig.file=shared/conf/application-production.conf > /dev/null 2>&1 &", pty=False)

wrapping up

今までのをまとめて、1つのfabfile.pyにした。長いのでgistの方に貼った(こちら)。

Fabricを使い始めたばっか&Pythonは詳しくないので、直した方が良い点とか指摘事項とかがあれば、コメントを頂ければ幸い。

TODO

今後、以下に対応したい。

  • dependenciesが更新された時にも、簡単に対応出来るようにする。
  • DB migrationの仕組み。evolutionsって使いにくすぎる・・・

まとめ

Ruby on Railsのプロジェクトで無ければ、CapistranoよりFabricの方がシンプルで良いのでは?ドキュメントも充実しているので、Pythonに詳しくない自分でも普通にやりたい事が出来たしオヌヌメ。

コメントを残す

メールアドレスが公開されることはありません。 が付いている欄は必須項目です