TL;DR
- Gem in a BoxのGemを別のGem in a Boxサーバーに同期する
- 新しくpushされたとき
- 削除されたとき
- Rack Middlewareでrsyncを使う
はじめに
Gem in a BoxのデータをRackのMiddlewareを使用して同期する方法を紹介します。
プライベートなGemサーバーをCIに組み込む時にGemデータを同期して冗長化する必要があったのでこの方法をとりました。
あくまでもデータ同期だけです。Active-Standbyを想定してます。
方針
やりたいこと
- Gem in a BoxにpushしたGemをpushされた時に同期する
- 削除された時も同期削除する
どうするか
- RackでPOSTリクエストまたはDELTEリクエストが来た時にrsyncでデータディレクトリを同期する
- 同期してからレスポンスを返す
- RackのMiddlewareで実現する
- Proxyしてるrubygems.orgのGemなんかはどうでもいい
そのために
- rsyncするRack Middlewareを使う
- rsyncするため Primary -> Secondary へSSH接続できるようにする
- Gem in a Box用のユーザを作る
- 秘密鍵・公開鍵のペアを適切に設定する
その他
- Unicornを使う
- systemdでUnicornを起動する
- systemdの環境ファイルにお互いのIPを持つ
- Rackは環境ファイルからの環境変数で自分がPrimaryかSecondaryか判断する
注意点
- rsyncなのでGem数が増えたとき時間かかるかも
- rubygems.orgをProxyしなければいい
- GETに比べてPOST/DELETEが少ない前提
- Unicornが落ちたらデータが同期できない
やってみる
Vagrantでやってみます。一式をGithubに用意しました。
https://github.com/nownabe/example-geminabox-replication
まずこのレポジトリをクローンしてください。
git clone https://github.com/nownabe/example-geminabox-replication
cd example-geminabox-replication
VagrantでGem ServerのPrimary/Secondaryとなる2台のVMを起動してプロビジョンします。
Rubyをビルドするので時間かかります。
vagrant up
このVagrantfileを使うと、次のIPアドレスで起動されます。
- Primary: 192.168.33.29:8080
- Secondary: 192.168.33.30:8080
まずブラウザで確認してみます。どちらのサーバーもGem in a Boxの画面が見れると思います。
適当なGemをPushしてみましょう。
$ gem push pkg/ringc-0.1.2.gem --host http://192.168.33.29:8080
Pushing gem to http://192.168.33.29:8080...
Gem ringc-0.1.2.gem received and indexed.
ブラウザを更新するとGemがpushされたことが確認できます。
Secondaryもrsyncによって同期されています。
削除も同期されます。
SecondaryからGemをインストールできることを確認してみます。
$ gem install ringc -s http://192.168.33.30:8080
Fetching: ringc-0.1.2.gem (100%)
Successfully installed ringc-0.1.2
Parsing documentation for ringc-0.1.2
Installing ri documentation for ringc-0.1.2
Done installing documentation for ringc after 0 seconds
1 gem installed
ばっちりですね!!
解説
具体的な方法はGithubのコードを読んでいただくのが一番だと思います。
重要な一部だけ説明します。
SSH
-
gem_server
ユーザを作成している -
gem_server
ユーザのホームディレクトリは/opt/gem_server
-
/opt/gem_server/.ssh
以下をごにょごにょやって Primary -> Secondary にSSHできるようにしている
config.ru
Gem in a Boxを起動するconfig.ru
は次のようになってます。
require "geminabox"
require "rack/rsync"
if ENV["GEMINABOX_PRIMARY"] == ENV["GEMINABOX_MYADDRESS"]
src = "/opt/gem_server/data/"
dst = "#{ENV['GEMINABOX_SECONDARY']}:/opt/gem_server/data/"
use Rack::Rsync, src, dst, "-a", "--delete" do |env|
env["REQUEST_METHOD"] == "POST" || env["REQUEST_METHOD"] == "DELETE"
end
end
Geminabox.data = "/opt/gem_server/data"
Geminabox.rubygems_proxy = true
run Geminabox
- systemdが読む環境ファイルから自分のIPがPrimaryのIPと一致するかチェックする
- 一致するときだけ
Rack::Rsync
というミドルウェアを使うようにする -
Rack::Rsync
にリクエストメソッドがPOSTかDELETEのときだけrsyncを実行するように条件を与えている
systemd
systemdのunitファイルは次のようになってます。
[Unit]
Description=Gem Server
[Service]
WorkingDirectory=/opt/gem_server/geminabox
ExecStart=/usr/local/bin/unicorn -p 8080
EnvironmentFile=/opt/gem_server/geminabox/environments
User=gem_server
Group=gem_server
[Install]
WantedBy=multi-user.target
なんの変哲もないですね。起動ユーザをgem_server
ユーザにして、環境ファイルを読むようにしてます。
Unicornが落ちたとき自動で復旧するようにRestart=always
とかしといてもいいかもしれません。
おわりに
タイミングシビアに同期したい場合にいかがでしょうか。
これだと不安だって場合はもっと下のレイヤーでやったほうがよさそうです。