読者です 読者をやめる 読者になる 読者になる
pixiv insideは移転しました! ≫ http://inside.pixiv.blog/

pixivのデプロイを支えるpploy

メリークリスマス。@edvakfです。

f:id:devpixiv:20140325045217p:plain

以前にpixivの開発・デプロイ環境の変遷(2014年春版)という記事を書いたのですが、その後もpixivのデプロイ環境は変化し続けています。

今日はpixivで使っているpployというツールについて、半分社内向け資料のつもりで無駄に詳しく書いてみたいと思います。ちょうど年末だし「社内属人コードのドキュメント充実化デイ」をやりたいよねーって話をしていたところでもありますし。

一度社内で行ったプレゼンから抜粋した8ページだけの小さなスライドも公開したので忙しい方はどうぞ。

pployとは

デプロイといえばcapistranoやminaなどのスクリプトを手動で実行している人もいると思いますが、pployはcapistranoなどの代替ツールではなく、サーバー上でcapistranoなどを実行するためのウェブアプリケーションです(webistranoの代替ツールと言えます)。開発はgithubで公開で行っています。

f:id:edvakf:20141225013720p:plain

pixivのデプロイ画面からのデプロイは昔から一貫してこのような手順で行われていました。

  1. 画面上でデプロイロックをかける
  2. masterにpush
  3. ステージングにデプロイ
  4. 本番にデプロイ
  5. 画面上でロックを解除

誰かがロック中は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_COMMITorigin/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というグループチャットに誰がデプロイ中になったかを通知することが出来ます。

f:id:edvakf:20141225013807p:plain

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もチラっと登場してニヤリとしました。

WEB+DB PRESS Vol.84

この記事はピクシブ株式会社のAdvent Calendarの25日目です。ピクシブでは技術とアイデアで僕達と一緒にサービスを育ててくれるエンジニアを募集しています。気になった方は、僕(@edvakf)にメンションをくれると遊びに来ていただけるよう取り図ります。

それでは良いお年を。

ピクシブ株式会社 Advent Calendar 2014 - Qiita