Play2アプリケーションのビルド・デプロイをFabricで自動化する


まだ手動でリリースしていた頃、”リリース職人”みたいな扱いになって作業押し付けられてばかりでカッとなって自動化した時の遺物を載せます。
実際使っているものを所々端折って書いていますが、だいたいこんな感じ。
パスは適宜読み替えてください。

# 前置き
## 準備するもの
* [Fabric](http://fabric-ja.readthedocs.org/ja/latest/) ビルド・デプロイの自動化に使う。
* [daemontools](http://cr.yp.to/daemontools.html) プロセス管理に使う。ここではdaemontoolsの設定については特に触れません。

## ディレクトリ構成

```
/[root]
　　　├ /service
　　　|　　└ run
　　　└ /product
　　　　　　├ current
　　　　　　└ /versions
　　　　　　　　　└ [version_number]
　　　　　　　　　　　　├ /bin
　　　　　　　　　　　　├ /conf
　　　　　　　　　　　　└ /lib
```

* ***/service/run***
	daemontoolsのサービス起動スクリプト

* ***/product/current***
	任意の ***/product/versions/[version_number]*** へのシンボリックリンク

* ***/procduct/versions/[version_number]***
	Play2アプリケーションが展開される場所
	**play stage**コマンドで生成されるものが展開されると思ってもらえればOK

## サービス起動スクリプト
daemontoolsのサービス起動スクリプトはこんな感じ。

```sh
exec [PATH_TO_ROOT]/product/current/bin/[MY_APP_NAME] -Dconfig.file=[PATH_TO_ROOT]/product/current/conf/application.conf
```


# ビルドの自動化
以前はJenkinsのシェルで実行していたものを、gitで管理できるようFabricのスクリプトにしています。
実際のスクリプトはこんな感じ。（これも所々端折ってます）

```python
from fabric.api import local, lcd, settings
from datetime import datetime


def build():
	play_stage()
	remove_non_deploy_files()
	make_tar_file()


# play stageコマンドを実行。Jenkins環境ならsbtを、開発環境ならローカルのplayコマンドを直で使う。
def play_stage():
	build_command = ("java -Dsbt.log.noformat=true -XX:MaxPermSize=256M "
                     "-jar [PATH_TO_SBT]/sbt-launch.jar "
                     "clean compile scalastyle stage") if is_jenkins() is True \
        else "play stage"
	local(build_command)


# 実行環境の判定
def is_jenkins():
    return '[JENKINS_HOSTNAME]' in local('hostname')


# READMEとshareは使わないので削除
def remove_non_deploy_files():
    with settings(warn_only=True):
        local('rm -r ./target/universal/stage/README*')
        local('rm -r ./target/universal/stage/share')


# stageで生成されたファイル郡を.tar.gz化。Jenkins環境なら、ビルド番号名に、それ以外なら日付をファイル名にする。
def make_tar_file():
    with lcd("./target/universal"):
        with settings(warn_only=True):
            local('pwd')
            build_number = get_build_number()
            local('rm -r ./%s' % build_number)
            local('mv ./stage ./%s' % build_number)
            local('tar cvzf %s.tar.gz ./%s' % (build_number, build_number))


# Jenkinsのビルド番号を取得。無ければ日付文字列を返す。
def get_build_number():
    with settings(warn_only=True):
        build_number = local('printenv BUILD_NUMBER', capture=True)
        if build_number.succeeded is True:
            return build_number
        else:
            return datetime.now().strftime('%Y%m%d')

```

これを **fab_build.py**として、以下のコマンドで実行できます。

```sh
fab -f fab_build.py build
```


# デプロイの自動化
ビルドと同じようにFabricスクリプト化してます。

```python
from fabric.api import run, put, env, cd, sudo


def release(file_path):
    env.hosts = ["xxx.xxx.xxx.xxx", "yyy.yyy.yyy.yyy"]
    env.user = '[USER]'
    env.key_filename = '[PATH_TO_PRIVATE_KEY]'
    deploy(file_path)


# デプロイの実行
def deploy(file_path):
	env.use_ssh_config = True
	build_number = file_name.split('.')[0]
	upload(file_path)
	expand(file_name)
	update_symbolic_link(target, build_number)
	restart_service(get_service(target))


# .tar.gzファイルをアップロード
def upload(file_path):
    put(file_path, "./product/versions/")


# .tar.gzファイルを展開
def expand(file_name):
    with cd("~/product/versions"):
        run("tar zxvf %s" % file_name)


# シンボリックリンクを更新
def update_symbolic_link(number):
    with cd("~/product"):
        run("ln -sfn [PATH_TO_ROOT]/product/versions/%s current" % (number))


# daemontoolsでプロセスを再起動
def restart_service():
    stop_service()
    start_service()


# プロセス停止
def stop_service():
    manage_service("down")


# プロセス起動
def start_service():
    manage_service("up")


# daemontoolsの実行
def manage_service(command):
    svc_opt = "-u" if command == "up" else "-d"
    sudo("[PATH_TO_DAEMONTOOLS]/command/svc %s [PATH_TO_DAEMONTOOLS]/service/[MY_APP_NAME]" % (svc_opt), pty=True, shell=False)
```

これも **fab_deploy.py**として、以下のコマンドで実行できます。

```sh
fab -f fab_deploy.py release:"./target/universal/[BUILD_NUMBER].tar.gz"
```

# Jenkinsの設定
ビルド・デプロイ用のジョブを作ったら、シェルでFabricのスクリプトを実行するようにします。
基本的には上のコマンドを並べただけですが、ビルド番号をJenkinsの環境変数から取得するようになっています。

```sh
fab -f fab_build.py build
fab -f fab_deploy.py release:"../target/universal/${BUILD_NUMBER}.tar.gz"
```

# 最後に
実際はもっと細かい制御も入れているのですが、以下略。

さらに、Slack + HubotでJenkinsのジョブを叩くコマンドを定義して、Slack経由でビルド・デプロイするようにしています。これで、Slackが使えれば、どこでも誰でもリリースができる！

もう「誰にも”リリース職人”なんて呼ばせないッ！！」