pixivのデプロイを支えるpploy
メリークリスマス。@edvakfです。
以前にpixivの開発・デプロイ環境の変遷(2014年春版)という記事を書いたのですが、その後もpixivのデプロイ環境は変化し続けています。
今日はpixivで使っているpployというツールについて、半分社内向け資料のつもりで無駄に詳しく書いてみたいと思います。ちょうど年末だし「社内属人コードのドキュメント充実化デイ」をやりたいよねーって話をしていたところでもありますし。
一度社内で行ったプレゼンから抜粋した8ページだけの小さなスライドも公開したので忙しい方はどうぞ。
pployとは
デプロイといえばcapistranoやminaなどのスクリプトを手動で実行している人もいると思いますが、pployはcapistranoなどの代替ツールではなく、サーバー上でcapistranoなどを実行するためのウェブアプリケーションです(webistranoの代替ツールと言えます)。開発はgithubで公開で行っています。
pixivのデプロイ画面からのデプロイは昔から一貫してこのような手順で行われていました。
- 画面上でデプロイロックをかける
- masterにpush
- ステージングにデプロイ
- 本番にデプロイ
- 画面上でロックを解除
誰かがロック中はmasterにはpushしてはいけないというルールと、masterの最新コミットは常に本番にデプロイされていなければいけない(戻す場合はrevertしてからデプロイする)というルールがあります。
pploy以前
pployの前のデプロイ画面は以下の様な特徴がありました。
- Apache+PHPのウェブアプリケーションと、デプロイスクリプトを実行するmonitという構成
- PHPではトリガーファイルを作成し、monitがトリガーファイルのmtimeを監視してデプロイ用スクリプトを叩く
- ウェブ画面のPHPの実行のユーザー(www-data)とデプロイスクリプトの実行ユーザー(deploy)が分かれているので、www-dataにアプリケーションサーバーの権限を与えなくてよい
- デプロイ用スクリプトは各リポジトリに置く
- そんなの当然と思われるでしょうが、数年前のpixivでは社内の全てのプロジェクトのデプロイを司る複雑なスクリプトがありました
- 同様にcheckoutスクリプトも各リポジトリにある
- 各リポジトリに
.deploy/htdocs/index.php
というファイルがあり、ウェブ画面上に任意の内容を表示させられる - 最近のコミットログや前回のデプロイから変更のあったファイルなどを表示させたければ
index.php
からgitを叩いてその結果を出力する必要がある
今考えるとおもしろい構成ですが、さらに前のデプロイの仕組みから考えると恐ろしく進歩していました。
ただ、実際に使ってみると不便もありました。
git fetch origin && git reset --hard origin/master && git clean -ffdx
などのお決まりのチェックアウトスクリプトが各リポジトリにコピペで置かれるindex.php
も作りこむのは結構面倒なので簡素なものをコピペしていた- monitのconfigなどを設置するので、新しくデプロイしたいプロジェクトを追加する手順が面倒な上に権限も必要
- PHPを実行するユーザーとデプロイスクリプトを実行するユーザーが分かれていることでの面倒
- 例えばdeployユーザーとして作ったログファイルをwww-dataとして
tail -f
していたが、書き出しが終わったのかどうかの判断ができない
pployの設計方針
上記の不便の解消すべく、このような設計方針を立てました。
- 各プロジェクトのデプロイスクリプトは各リポジトリに置く方針は変わらず
- pploy自体はデプロイスクリプトの実行ユーザーと同じユーザーで動かす
- deployユーザーに鍵を置くことになるが、debianのapacheデフォルトのwww-dataユーザーに権限があるよりは安全
- デプロイスクリプトの実行→ログのストリーム表示は単純なフォームのPOSTで済ませる
- チェックアウトなど、git操作関係はpployが担当する
- 前までは(やろうと思えば)git以外のプロジェクトにも対応できたが、そこは諦める
- ウェブ画面に表示する直近のコミットログや前回のデプロイ実行ログもpployが出す
- それ以外の、ステージング環境のURLへのリンクなど各リポジトリに固有のものは、別途readme.htmlというのを置けるようにする
- 新規にデプロイするプロジェクトの追加も画面からできる
pployはScalaで書きました。「pploy自体はデプロイスクリプトの実行ユーザーと同じユーザーで動かす」ということで、それまでのApache+PHPの線が無くなったので流行りものを試してみたかったのと、Play FrameworkのチュートリアルにTransfer-Encoding: chunked
のことが書いてあったり、JGitが使えるだろうと思ったからです。
結局JGitはシンボリックリンクをうまく扱えないなどの問題があったのであまり使ってなくてgitコマンドとのハイブリッドになっていますが。
pployを導入してみて
いくつか印象に残っていることを列挙していきます。
これまでのデプロイ画面からの移行
デプロイスクリプトはこれまでの各リポジトリのものをまったく変えずに使えたので、これまでのデプロイ画面に乗っていたものは簡単に移せました。
デプロイサーバーの引っ越し
monitのconfigなどを置かなくても良くなったし、画面からデプロイ対象プロジェクトの追加も簡単にできるようになったので、デプロイサーバーの引っ越しがあっさりできました。
パスの変更
社内でgateという認証付きプロキシを使うことになって、デプロイ画面のURLのパス部分が /
から /deploy/
に変わった時に、Play Frameworkのreverse routing(コントローラー+アクション名からURLを引けるやつ)を使ったおかげで、設定で application.context=/deploy/
と指定するだけでパスの変更に対応できました。
このあたりは@catatsuyによる記事にちらっと出てきます。
ログがストリーミングされなかった
デプロイスクリプト、Play Framework、gate、nginxのすべての層でハマって、一つ一つ解決していった結果、デプロイログがリアルタイムで見られるようになりました。(OpenJDK-7のバグにもハマってOpenJDK-6にしたりしました)
その経緯も@catatsuyがまとめてくれています。
pployの機能
READMEに書いてあるもの、ないもの含めて(現時点での)pployの機能を紹介したいと思います。
デプロイ対象のリポジトリの構成
. ├── .deploy │ ├── bin │ │ └── deploy │ └── config │ └── readme.html ├── その他のプロジェクトのファイル
最小はこんな感じです。readme.htmlも別に置かなくても良いですが、ある場合はこの記事の一番上の画像のように表示されます。
デプロイスクリプト
さきほどから「デプロイスクリプト」と呼んでいたものは、リポジトリ内の.deploy/bin/deploy
のことです。
デプロイスクリプトは実行さえできれば何で書かれていてもよくて、社内ではシェルスクリプトで書かれているものが多いです。pixiv自体はPHPでデプロイスクリプトが書かれていましたが、最近シェルスクリプトのみになりました。
試していませんが、capistranoでデプロイするプロジェクトの場合は.deploy/bin/deploy
に
#!/bin/bash cd $(dirname $0) cd ../.. bundle install --path=vendor/bundle bundle exec cap production deploy
などと書けば対応できると思います。
デプロイスクリプトにはこのような感じで環境変数が渡されます。
DEPLOY_ENV=production DEPLOY_USER=edvakf .deploy/bin/deploy
DEPLOY_ENVはstagingとproductionのみで決め打ちですが、プロジェクト内にconfigを置けるようにしようと思っています。
チェックアウトスクリプト
デフォルトではチェックアウトはこのようになスクリプトで行われます。
#!/bin/bash -eux git fetch --prune --depth 20 git reset --hard $DEPLOY_COMMIT git clean -fdx git submodule sync git submodule init git submodule update --recursive
DEPLOY_COMMIT
はorigin/master
など、デプロイ画面からPOSTするパラメーターです。
あまり強くはサポートしていませんが、デフォルトのチェックアウトスクリプトで不十分な場合はリポジトリ内に.deploy/bin/checkout_overwrite
スクリプトを置いてもらうことになっています。
デプロイするユーザー名
画面上で一人がデプロイ中である場合は他の人はデプロイ中になれません。「◯◯さんがデプロイ中です」のようなロック状態を表すのにデプロイユーザーという概念があります。application.confにデフォルトで
pploy.users=["foo", "bar"]
と書いてあるのでこれを変更するか、LDAPから一覧を取得できるようにも設定できます。以下の設定例は、"someone"というユーザーでLDAPを叩き、"deployers"というグループにいるユーザー一覧を取得する設定になっています。LDAPから取得した一覧はこの例では3600秒キャッシュされます。
pploy.ldap.url="ldap://ldap.example.com:389" pploy.ldap.login="cn=someone,dc=example,dc=com" pploy.ldap.password="SomeonesPassword" pploy.ldap.search="dc=deployers,dc=example,dc=com" pploy.ldap.cachettl=3600
作業ディレクトリ
デプロイする対象のプロジェクトをcloneしてきたり、ログファイルの置かれるディレクトリです。
デフォルトの作業ディレクトリはこのような設定になっていますが、
pploy.dir="/tmp/pploy"
/tmp
以下のファイルは一定時間がたつとファイルが消されてしまうので、pixivでは/home/deploy/pploy-working-dir
にしています。
idobata通知
idobataというグループチャットに誰がデプロイ中になったかを通知することが出来ます。
application.confのこちらの項目です。
pploy.idobata.endpoint="https://idobata.io/hook/generic/11112222-3333-4444-5555-666677778888"
画面に表示する最新コミットログの数
この設定です。
pploy.commits.length=20
この数字はチェックアウト時に git fetch --prune --depth 20
のようにも使われます。これによって、コミット数やブランチ数が増えるとJGitのgit log
相当が遅くなる問題も回避しています。
ロック時間
画面上でロックを取得しても、一定時間のうちに解除しなければ自動的に解除されます。
pploy.lock.gainMinutes=20 # ロック取得時間(分) pploy.lock.extendMinutes=10 # ロック延長時間(分)
pploy自体のデプロイ
社内ではpploy-distというプロジェクトを別に用意して、pploy上からpploy-distのデプロイを実行するとpployが更新されるという運用をしています。
pploy-dist ├── .deploy │ └── bin │ └── deploy ├── README.md ├── monit │ └── pploy.conf ├── pploy.sh └── ssh └── config
pploy-distの.deploy/bin/deploy
はJenkinsがビルドしたpployのバイナリをzipで取ってきて展開して/home/deploy/pploy
以下に置きます。Jenkinsを挟むのが面倒なのでデプロイサーバー上でビルドするように変えるかも。
pploy.shはpployの起動スクリプトです。application.confの設定をコマンドラインオプションで上書きしてpployを起動しています。
#!/bin/bash PIDFILE=/home/deploy/pploy.pid # composer install に$HOMEが必要で、monit経由だとこれがセットされないっぽいため export USER=deploy export HOME=/home/$USER case "$1" in "start" ) /home/deploy/pploy/bin/pploy \ -Dpidfile.path=$PIDFILE \ -Dapplication.langs=ja,en \ -Dapplication.context=/deploy/ \ -Dpploy.lock.gainMinutes=10 \ -Dpploy.dir=/home/deploy/pploy-working-dir \ -Dhttp.port=9000 ;; "stop" ) [ -f $PIDFILE ] && kill $(cat $PIDFILE) ;; "*" ) echo "usage: $0 start|stop" esac
pploy自体はmonitで監視しているので、monitのconfigが置いてあります。monitがpploy.shを叩いてくれます。
pployのプロセスが無ければmonitが自動的に立ち上げてくれるというのを利用して、pploy-distのデプロイスクリプトはpploy.sh stop
してプロセスを殺して終了します。実行中のデプロイスクリプトがあるかもしれないので、誰か他の人がデプロイ中でないことを確認してからpployをデプロイすることにしています。
pploy-distの中にあるssh/config
は、JGitが$HOME/.ssh/config
は見てくれるけど/etc/ssh/ssh_config
は見てくれなかったので作りました。monit/pploy.conf
と同じでサーバーに一番最初にpployをセットアップするときにコピーすれば十分です。
おわりに
ちょうど1年ほど前に、pixivではデプロイや開発環境やサービス全体の開発効率の改善に携わる「プラットフォームチーム」を僕自身が猛烈にプッシュして設立しました。僕達のサービスも、サービスを取り巻く技術も常に進化し続けており、巨大なコードベースをどうやって維持していくかを考えるのはとてもチャレンジングで面白い仕事だと思っています。
デプロイ改善はプラットフォームチームが今年取り組んだ中でも大きなもののうちの1つです。宣伝になるのですが、今年pixivが実際に遭遇して解決してきた大規模PHPアプリケーションのデプロイのノウハウをすべてプラットフォームチームの@tototoshiが一昨日発売の WEB+DB PRESS Vol. 84 に書いてくれました。pployもチラっと登場してニヤリとしました。
この記事はピクシブ株式会社のAdvent Calendarの25日目です。ピクシブでは技術とアイデアで僕達と一緒にサービスを育ててくれるエンジニアを募集しています。気になった方は、僕(@edvakf)にメンションをくれると遊びに来ていただけるよう取り図ります。
それでは良いお年を。